diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..19282163 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +** + +# Direct mirror of Dockerfile `COPY` contents + +!vendor/** +!scripts/Linux-Build.sh +!premake5.lua +!Dependencies.lua + +!StarEditorLauncher/** +!StarEngine-Launcher/** +!StarEngine-ScriptCore/** +!StarEngine-Runtime/** + +!StarEditor/src/** +!StarEditor/premake5.lua +!StarEditor/Resources/LUA/** +!StarEditor/SandboxProject/premake5.lua +!StarEditor/SandboxProject/Assets/Scripts/** + +!StarEngine/** diff --git a/.editorconfig b/.editorconfig index 01689820..191a1451 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,314 @@ -# top-most EditorConfig file +# Visual Studio generated .editorconfig file with C++ settings. root = true -# Unix-style newlines with a newline ending every file -[*] -end_of_line = lf +[*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] + +indent_style = tab +tab_width = 4 +indent_size = 4 insert_final_newline = true -indent_style = tab \ No newline at end of file + +# Visual C++ Code Style settings + +cpp_generate_documentation_comments = xml + +# Visual C++ Formatting settings + +cpp_indent_braces = false +cpp_indent_multi_line_relative_to = innermost_parenthesis +cpp_indent_within_parentheses = indent +cpp_indent_preserve_within_parentheses = true +cpp_indent_case_contents = true +cpp_indent_case_labels = true +cpp_indent_case_contents_when_block = false +cpp_indent_lambda_braces_when_parameter = false +cpp_indent_goto_labels = one_left +cpp_indent_preprocessor = leftmost_column +cpp_indent_access_specifiers = false +cpp_indent_namespace_contents = true +cpp_indent_preserve_comments = false +cpp_new_line_before_open_brace_namespace = same_line +cpp_new_line_before_open_brace_type = new_line +cpp_new_line_before_open_brace_function = new_line +cpp_new_line_before_open_brace_block = new_line +cpp_new_line_before_open_brace_lambda = ignore +cpp_new_line_scope_braces_on_separate_lines = true +cpp_new_line_close_brace_same_line_empty_type = true +cpp_new_line_close_brace_same_line_empty_function = true +cpp_new_line_before_catch = true +cpp_new_line_before_else = true +cpp_new_line_before_while_in_do_while = true +cpp_space_before_function_open_parenthesis = remove +cpp_space_within_parameter_list_parentheses = false +cpp_space_between_empty_parameter_list_parentheses = false +cpp_space_after_keywords_in_control_flow_statements = true +cpp_space_within_control_flow_statement_parentheses = false +cpp_space_before_lambda_open_parenthesis = false +cpp_space_within_cast_parentheses = false +cpp_space_after_cast_close_parenthesis = false +cpp_space_within_expression_parentheses = false +cpp_space_before_block_open_brace = true +cpp_space_between_empty_braces = false +cpp_space_before_initializer_list_open_brace = false +cpp_space_within_initializer_list_braces = true +cpp_space_preserve_in_initializer_list = true +cpp_space_before_open_square_bracket = false +cpp_space_within_square_brackets = false +cpp_space_before_empty_square_brackets = false +cpp_space_between_empty_square_brackets = false +cpp_space_group_square_brackets = true +cpp_space_within_lambda_brackets = false +cpp_space_between_empty_lambda_brackets = false +cpp_space_before_comma = false +cpp_space_after_comma = true +cpp_space_remove_around_member_operators = true +cpp_space_before_inheritance_colon = true +cpp_space_before_constructor_colon = true +cpp_space_remove_before_semicolon = true +cpp_space_after_semicolon = true +cpp_space_remove_around_unary_operator = true +cpp_space_around_binary_operator = insert +cpp_space_around_assignment_operator = insert +cpp_space_pointer_reference_alignment = left +cpp_space_around_ternary_operator = insert +cpp_wrap_preserve_blocks = one_liners + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 3 +indent_style = tab +tab_width = 3 + +# New line preferences +end_of_line = crlf +insert_final_newline = 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 +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +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_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### 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 = one_less_than_current +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.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 = + +# 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 + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c36b047..b10cdfc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,30 @@ # Binaries **/bin/ +!vendor/bin/ +!StarEngine/vendor/assimp/bin +!StarEngine/vendor/mono/bin bin-int/ +obj/ +**/cache/** +**/Cache/** +StarEditor/vendor/VulkanSDK +StarEditor/Resources/Scripts/StarEngine-ScriptCore.dll +StarEditor/Resources/Scripts/StarEngine-ScriptCore.pdb +StarEditor/StarEditor Intermediates/ +Binaries +StarEditor/Resources/Scripts/ +__pycache__ -# StarEngine files -*.log -*imgui.ini +StarEditor/SandboxProject/Assets/Scripts/Binaries/** +StarEditor/SandboxProject/Assets/SoundBank.hsb -!StarEditor/imgui.ini +# StarEditor ApplicationSettings +StarEditor/Config/ApplicationSettings.yaml + +# StarEditor Folders +StarEditor/Captures/ +StarEditor/Tools/JoltViewer # Visual Studio files and folder .vs/ @@ -16,14 +33,43 @@ Intermediates/ **.vcxproj.filters **.vcxproj.user **.csproj +**.nsight-gfxproj +**.DotSettings +**.user -# Directories -StarEngine/vendor/VulkanSDK -StarEditor/assets/cache -scripts/__pycache__ +.idea/ +Makefile +*.make +*.a -StarEngine/vendor/VulkanSDK/VulkanSDK-1.2.170.0-Installer.exe -vendor/premake/bin/ -StarEditor/Resources/Scripts/StarEngine-ScriptCore.dll -StarEditor/Resources/Scripts/StarEngine-ScriptCore.pdb -StarEditor/SandboxProject/Assets/Scripts/Binaries +StarEditor/EditorLayout.yaml +StarEditor/Config/EditorApplicationSettings.yaml + +# LFS Folders +lfs/ +hooks/ + +# Meta Files +**.meta + +# Log files +**.log + +# Auto saves +*.auto + +# Dumps +*.nvdbg +*.nv-gpudmp +*gpudmp.json + +# Folder Exceptions +!StarEditor/SandboxProject/Assets/Scripts/Binaries + +# System DLLs +StarEditor/SandboxProject/Assets/Scripts/Binaries/System*.dll +StarEditor/SandboxProject/Assets/Scripts/Binaries/Microsoft*.dll + +# File Exceptions +!AssetRegistryCache.hzr +StarEditor/DotNet \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index c6c8e2c7..2d8d8457 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,28 +1,49 @@ [submodule "StarEngine/vendor/spdlog"] path = StarEngine/vendor/spdlog - url = ../../gabime/spdlog.git - branch = v1.x + url = https://github.com/gabime/spdlog.git + [submodule "StarEngine/vendor/GLFW"] path = StarEngine/vendor/GLFW - url = ../../starbounded-dev/glfw.git - branch = master -[submodule "StarEngine/vendor/imgui"] - path = StarEngine/vendor/imgui - url = ../../starbounded-dev/imgui.git - branch = docking + url = https://github.com/TheCherno/glfw + [submodule "StarEngine/vendor/glm"] path = StarEngine/vendor/glm - url = ../../g-truc/glm.git - branch = master -[submodule "StarEngine/vendor/yaml-cpp"] - path = StarEngine/vendor/yaml-cpp - url = https://github.com/sheazywi/yaml-cpp -[submodule "StarEngine/vendor/imguizmo"] - path = StarEngine/vendor/imguizmo - url = https://github.com/sheazywi/imguizmo.git + url = https://github.com/g-truc/glm + [submodule "StarEngine/vendor/Box2D"] path = StarEngine/vendor/Box2D - url = https://github.com/sheazywi/box2d + url = https://github.com/TheCherno/Box2D + [submodule "StarEngine/vendor/msdf-atlas-gen"] path = StarEngine/vendor/msdf-atlas-gen url = https://github.com/sheazywi/msdf-atlas-gen + +[submodule "StarEngine/vendor/tracy/tracy"] + path = StarEngine/vendor/tracy/tracy + url = https://github.com/wolfpld/tracy/ + +[submodule "StarEngine/vendor/imgui"] + path = StarEngine/vendor/imgui + url = https://github.com/StudioCherno/imgui + branch = hazel + +[submodule "StarEngine/vendor/yaml-cpp"] + path = StarEngine/vendor/yaml-cpp + url = https://github.com/sheazywi/yaml-cpp + +[submodule "StarEngine/vendor/NFD-Extended/NFD-Extended"] + path = StarEngine/vendor/NFD-Extended/NFD-Extended + url = https://github.com/btzy/nativefiledialog-extended + +[submodule "StarEngine/vendor/backward-cpp"] + path = StarEngine/vendor/backward-cpp + url = https://github.com/bombela/backward-cpp + +[submodule "StarEngine/vendor/JoltPhysics/JoltPhysics"] + path = StarEngine/vendor/JoltPhysics/JoltPhysics + url = https://github.com/jrouwe/JoltPhysics + +[submodule "StarEngine/vendor/nvrhi"] + path = StarEngine/vendor/nvrhi + url = https://github.com/StudioCherno/nvrhi + branch = hazel \ No newline at end of file diff --git a/Dependencies.lua b/Dependencies.lua index 6abebcf7..eb9c3c44 100644 --- a/Dependencies.lua +++ b/Dependencies.lua @@ -1,48 +1,292 @@ --- StarEngine Dependencies +include "./vendor/premake_customization/ordered_pairs.lua" +-- Utility function for converting the first character to uppercase +function firstToUpper(str) + return (str:gsub("^%l", string.upper)) +end + +-- Grab Vulkan SDK path VULKAN_SDK = os.getenv("VULKAN_SDK") -IncludeDir = {} -IncludeDir["stb_image"] = "%{wks.location}/StarEngine/vendor/stb_image" -IncludeDir["yaml_cpp"] = "%{wks.location}/StarEngine/vendor/yaml-cpp/include" -IncludeDir["Box2D"] = "%{wks.location}/StarEngine/vendor/Box2D/include" -IncludeDir["GLFW"] = "%{wks.location}/StarEngine/vendor/GLFW/include" -IncludeDir["GLAD"] = "%{wks.location}/StarEngine/vendor/GLAD/include" -IncludeDir["ImGui"] = "%{wks.location}/StarEngine/vendor/imgui" -IncludeDir["ImGuizmo"] = "%{wks.location}/StarEngine/vendor/imguizmo" -IncludeDir["glm"] = "%{wks.location}/StarEngine/vendor/glm" -IncludeDir["filewatch"] = "%{wks.location}/StarEngine/vendor/filewatch" -IncludeDir["entt"] = "%{wks.location}/StarEngine/vendor/entt/include" -IncludeDir["mono"] = "%{wks.location}/StarEngine/vendor/mono/include" -IncludeDir["shaderc"] = "%{wks.location}/StarEngine/vendor/shaderc/include" -IncludeDir["SPIRV_Cross"] = "%{wks.location}/StarEngine/vendor/SPIRV-Cross" -IncludeDir["VulkanSDK"] = "%{VULKAN_SDK}/Include" -IncludeDir["msdfgen"] = "%{wks.location}/StarEngine/vendor/msdf-atlas-gen/msdfgen" -IncludeDir["msdf_atlas_gen"] = "%{wks.location}/StarEngine/vendor/msdf-atlas-gen/msdf-atlas-gen" -IncludeDir["miniaudio"] = "%{wks.location}/StarEngine/vendor/miniaudio/include" - -LibraryDir = {} - -LibraryDir["VulkanSDK"] = "%{VULKAN_SDK}/Lib" -LibraryDir["Mono"] = "%{wks.location}/StarEngine/vendor/mono/lib/%{cfg.buildcfg}" - -Library = {} -Library["mono"] = "%{LibraryDir.Mono}/libmono-static-sgen.lib" -Library["Vulkan"] = "%{LibraryDir.VulkanSDK}/vulkan-1.lib" -Library["VulkanUtils"] = "%{LibraryDir.VulkanSDK}/VkLayer_utils.lib" - -Library["ShaderC_Debug"] = "%{LibraryDir.VulkanSDK}/shaderc_sharedd.lib" -Library["SPIRV_Cross_Debug"] = "%{LibraryDir.VulkanSDK}/spirv-cross-cored.lib" -Library["SPIRV_Cross_GLSL_Debug"] = "%{LibraryDir.VulkanSDK}/spirv-cross-glsld.lib" -Library["SPIRV_Tools_Debug"] = "%{LibraryDir.VulkanSDK}/SPIRV-Toolsd.lib" - -Library["ShaderC_Release"] = "%{LibraryDir.VulkanSDK}/shaderc_shared.lib" -Library["SPIRV_Cross_Release"] = "%{LibraryDir.VulkanSDK}/spirv-cross-core.lib" -Library["SPIRV_Cross_GLSL_Release"] = "%{LibraryDir.VulkanSDK}/spirv-cross-glsl.lib" - - --- Windows -Library["WinSock"] = "Ws2_32.lib" -Library["WinMM"] = "Winmm.lib" -Library["WinVersion"] = "Version.lib" -Library["BCrypt"] = "Bcrypt.lib" +--[[ + If you're adding a new dependency all you have to do to get it linking + and included is define it properly in the table below, here's some example usage: + + MyDepName = { + LibName = "my_dep_name", + LibDir = "some_path_to_dependency_lib_dir", + IncludeDir = "my_include_dir", + Windows = { DebugLibName = "my_dep_name_debug" }, + Configurations = "Debug,Release" + } + + MyDepName - This is just for organizational purposes, it doesn't actually matter for the build process + LibName - This is the name of the .lib file that you want e.g Hazelnut to link against (you shouldn't include the .lib extension since Linux uses .a) + LibDir - Indicates which directory the lib file is located in, this can include "%{cfg.buildcfg}" if you have a dedicated Debug / Release directory + IncludeDir - Pretty self explanatory, the filepath that will be included in externalincludedirs + Windows - This defines a platform-specific scope for this dependency, anything defined in that scope will only apply for Windows, you can also add one for Linux + DebugLibName - Use this if the .lib file has a different name in Debug builds (e.g "shaderc_sharedd" vs "shaderc_shared") + Configurations - Defines a list of configurations that this dependency should be used in, if no Configurations is specified all configs will link and include this dependency (which is what we want in most cases) + + Most of the properties I've listed can be used in either the "global" dependency scope OR in a specific platform scope, + the only property that doesn't support that is "Configurations" which HAS to be defined in the global scope. + + Of course you can put only SOME properties in a platform scope, and others in the global scope if you want to. + + Naturally I suggest taking a look at existing dependency definitions when adding a new dependency. + + Remember that in most cases you only need to update this list, no need to manually add dependencies to the "links" list or "includedirs" list + + HEADER-ONLY LIBRARIES: If your dependency is header-only you shouldn't specify e.g LibName, just add IncludeDir and it'll be treated like a header-only library + +]]-- + +Dependencies = { + Vulkan = { + Windows = { + LibName = "vulkan-1", + IncludeDir = "%{VULKAN_SDK}/Include/", + LibDir = "%{VULKAN_SDK}/Lib/", + }, + Linux = { + LibName = "vulkan", + IncludeDir = "%{VULKAN_SDK}/include/", + LibDir = "%{VULKAN_SDK}/lib/", + }, + }, + DirectXCompiler = { + LibName = "dxcompiler", + }, + TBB = { + Linux = { LibName = "tbb" }, + }, + NvidiaAftermath = { + LibName = "GFSDK_Aftermath_Lib.x64", + IncludeDir = "%{wks.location}/StarEngine/vendor/NvidiaAftermath/include", + Windows = { LibDir = "%{wks.location}/StarEngine/vendor/NvidiaAftermath/lib/x64/windows/" }, + Linux = { LibDir = "%{wks.location}/StarEngine/vendor/NvidiaAftermath/lib/x64/linux/" }, + }, + Assimp = { + IncludeDir = "%{wks.location}/StarEngine/vendor/assimp/include", + Windows = { LibName = "assimp-vc143-mt", DebugLibName = "assimp-vc143-mtd", LibDir = "%{wks.location}/StarEngine/vendor/assimp/bin/windows/%{cfg.buildcfg}/" }, + Linux = { LibName = "assimp", LibDir = "%{wks.location}/StarEngine/vendor/assimp/bin/linux/" }, + Configurations = "Debug,Release" + }, + ShaderC = { + LibName = "shaderc_shared", + Windows = { DebugLibName = "shaderc_sharedd", }, + IncludeDir = "%{wks.location}/StarEngine/vendor/shaderc/include", + Configurations = "Debug,Release" + }, + ShaderCUtil = { + LibName = "shaderc_util", + Windows = { DebugLibName = "shaderc_utild", }, + IncludeDir = "%{wks.location}/StarEngine/vendor/shaderc/libshaderc_util/include", + Configurations = "Debug,Release" + }, + SPIRVCrossCore = { + LibName = "spirv-cross-core", + Windows = { DebugLibName = "spirv-cross-cored", }, + Configurations = "Debug,Release" + }, + SPIRVCrossGLSL = { + LibName = "spirv-cross-glsl", + Windows = { DebugLibName = "spirv-cross-glsld", }, + Configurations = "Debug,Release" + }, + SPIRVTools = { + LibName = "SPIRV-Tools", + Windows = { DebugLibName = "SPIRV-Toolsd", }, + Configurations = "Debug,Release" + }, + GLFW = { + -- No need to specify LibDir for GLFW since it's automatically handled by premake + LibName = "GLFW", + IncludeDir = "%{wks.location}/StarEngine/vendor/GLFW/include", + }, + GLM = { + IncludeDir = "%{wks.location}/StarEngine/vendor/glm", + }, + EnTT = { + IncludeDir = "%{wks.location}/StarEngine/vendor/entt/include", + }, + Coral = { + LibName = "Coral.Native", + IncludeDir = "%{wks.location}/StarEngine/vendor/Coral/Coral.Native/Include" + }, + ImGui = { + LibName = "ImGui", + IncludeDir = "%{wks.location}/StarEngine/vendor/imgui", + }, + ImGuiNodeEditor = { + IncludeDir = "%{wks.location}/StarEngine/vendor/imgui-node-editor", + }, + NFDExtended = { + LibName = "NFD-Extended", + IncludeDir = "%{wks.location}/StarEngine/vendor/NFD-Extended/NFD-Extended/src/include" + }, + NVRHI = { + LibName = "NVRHI", + IncludeDir = "%{wks.location}/StarEngine/vendor/nvrhi/include" + }, + FastNoise = { + IncludeDir = "%{wks.location}/StarEngine/vendor/FastNoise", + }, + MiniAudio = { + IncludeDir = "%{wks.location}/StarEngine/vendor/miniaudio/include", + }, + Farbot = { + IncludeDir = "%{wks.location}/StarEngine/vendor/farbot/include", + }, + Choc = { + IncludeDir = "%{wks.location}/StarEngine/vendor/choc", + }, + MagicEnum = { + IncludeDir = "%{wks.location}/StarEngine/vendor/magic_enum/include", + }, + Box2D = { + LibName = "Box2D", + IncludeDir = "%{wks.location}/StarEngine/vendor/Box2D/include", + }, + Tracy = { + LibName = "Tracy", + IncludeDir = "%{wks.location}/StarEngine/vendor/tracy/tracy/public", + }, + JoltPhysics = { + LibName = "JoltPhysics", + IncludeDir = "%{wks.location}/StarEngine/vendor/JoltPhysics/JoltPhysics", + }, + MSDFAtlasGen = { + LibName = "msdf-atlas-gen", + IncludeDir = "%{wks.location}/StarEngine/vendor/msdf-atlas-gen/msdf-atlas-gen", + }, + MSDFGen = { + LibName = "msdfgen", + IncludeDir = "%{wks.location}/StarEngine/vendor/msdf-atlas-gen/msdfgen", + }, + Freetype = { + LibName = "freetype" + }, + STB = { + IncludeDir = "%{wks.location}/StarEngine/vendor/stb/include", + }, + YAML_CPP = { + IncludeDir = "%{wks.location}/StarEngine/vendor/yaml-cpp/include", + }, + WS2 = { + Windows = { LibName = "ws2_32", }, + }, + Dbghelp = { + Windows = { LibName = " Dbghelp" }, + }, + FileWatch = { + IncludeDir = "%{wks.location}/StarEngine/vendor/filewatch/include" + }, + CDT = { + IncludeDir = "%{wks.location}/StarEngine/vendor/CDT" + }, + RTM = { + IncludeDir = "%{wks.location}/StarEngine/vendor/rtm/include" + }, + ACL = { + IncludeDir = "%{wks.location}/StarEngine/vendor/acl/include" + }, + BackwardCPP = { + IncludeDir = "%{wks.location}/StarEngine/vendor/backward-cpp" + }, + SPDLOG = { + IncludeDir = "%{wks.location}/StarEngine/vendor/spdlog/include" + }, +} + +-- NOTE(Peter): Probably don't touch these functions unless you know what you're doing (or just ask me if you need help extending them) + +function LinkDependency(table, is_debug, target) + + -- Setup library directory + if table.LibDir ~= nil then + libdirs { table.LibDir } + end + + -- Try linking + local libraryName = nil + if table.LibName ~= nil then + libraryName = table.LibName + end + + if table.DebugLibName ~= nil and is_debug and target == "Windows" then + libraryName = table.DebugLibName + end + + if libraryName ~= nil then + links { libraryName } + return true + end + + return false +end + +function AddDependencyIncludes(table) + if table.IncludeDir ~= nil then + externalincludedirs { table.IncludeDir } + end +end + +function ProcessDependencies(config_name) + local target = firstToUpper(os.target()) + + for key, libraryData in orderedPairs(Dependencies) do + + -- Always match config_name if no Configurations list is specified + local matchesConfiguration = true + + if config_name ~= nil and libraryData.Configurations ~= nil then + matchesConfiguration = string.find(libraryData.Configurations, config_name) + end + + local isDebug = config_name == "Debug" + + if matchesConfiguration then + local continueLink = true + + -- Process Platform Scope + if libraryData[target] ~= nil then + continueLink = not LinkDependency(libraryData[target], isDebug, target) + AddDependencyIncludes(libraryData[target]) + end + + -- Process Global Scope + if continueLink then + LinkDependency(libraryData, isDebug, target) + end + + AddDependencyIncludes(libraryData) + end + + end +end + +function IncludeDependencies(config_name) + local target = firstToUpper(os.target()) + + for key, libraryData in orderedPairs(Dependencies) do + + -- Always match config_name if no Configurations list is specified + local matchesConfiguration = true + + if config_name ~= nil and libraryData.Configurations ~= nil then + matchesConfiguration = string.find(libraryData.Configurations, config_name) + end + + if matchesConfiguration then + -- Process Global Scope + AddDependencyIncludes(libraryData) + + -- Process Platform Scope + if libraryData[target] ~= nil then + AddDependencyIncludes(libraryData[target]) + end + end + + end +end diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3c1f9d32 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,78 @@ +# syntax=docker/dockerfile:1 +FROM ubuntu:24.04 AS build + +RUN apt-get update + +RUN mkdir -p /workspace +WORKDIR /workspace + +### Fetch Remote Artifacts + RUN apt-get install -y curl xz-utils + + # The Ubuntu premake package only provides premake4 for some reason + ARG PREMAKE_TAG=5.0.0-beta2 + RUN curl -Lo premake.tar.gz https://github.com/premake/premake-core/releases/download/v$PREMAKE_TAG/premake-$PREMAKE_TAG-linux.tar.gz + RUN tar xzf premake.tar.gz + + # `Linux-Build` wants premake on the path + ENV PATH="/workspace:${PATH}" + + # Download and extract Vulkan SDK + ARG VULKAN_VER=1.3.283.0 + RUN curl -Lo vulkan.tar.xz https://sdk.lunarg.com/sdk/download/$VULKAN_VER/linux/vulkansdk-linux-x86_64-$VULKAN_VER.tar.xz + RUN tar xJf vulkan.tar.xz + + ENV VULKAN_SDK=/workspace/$VULKAN_VER/x86_64 + +### Install Build Environment + # Install basic build tooling + # `mold` is a faster linker than basic `ld` or llvm `lld` + # The llvm package provides `llvm-ar` which is a faster archiver than GNU + # binutils `ar` + RUN apt-get install -y build-essential clang mold llvm libtbb-dev + + # Install dotnet + RUN apt-get install -y dotnet-sdk-8.0 + + ## Install build dependencies + # GLFW + RUN apt-get install -y xorg-dev + # NFD + RUN apt-get install -y libgtk-3-dev + # BackwardCPP + RUN apt-get install -y libdw-dev libunwind-dev + +### Build Source Tree + # Copying subsets of the source tree can improve caching behaviour + # These should be ordered from least to most frequently modified + COPY vendor vendor + COPY scripts/Linux-Build.sh scripts/ + COPY premake5.lua . + COPY Dependencies.lua . + # + COPY StarEditorLauncher StarEditorLauncher + COPY StarEngine-Launcher StarEngine-Launcher + COPY StarEngine-ScriptCore StarEngine-ScriptCore + COPY StarEngine-Runtime StarEngine-Runtime + + ## StarEditor + # We don't need Resources etc. for the build and it massively increases + # image size and build time + COPY StarEditor/src StarEditor/src + COPY StarEditor/premake5.lua StarEditor/ + COPY StarEditor/Resources/LUA StarEditor/Resources/LUA + COPY StarEditor/SandboxProject/premake5.lua StarEditor/SandboxProject/ + COPY StarEditor/SandboxProject/Assets/Scripts StarEditor/SandboxProject/Assets/Scripts + + COPY StarEngine StarEngine + +## Execute The Build + ARG NPROC=1 + ARG CXXFLAGS= + ARG BUILD_CONFIG=Debug + + ENV STARENGINE_DIR=/workspace + ENV BUILD_CONFIG=$BUILD_CONFIG + + # This script is separate for use in non-containerized builds + RUN scripts/Linux-Build.sh AR=llvm-ar LDFLAGS=-fuse-ld=mold CXXFLAGS=$CXXFLAGS -j$NPROC diff --git a/Resources/Branding/StarStudioLogo.png b/Resources/Branding/StarStudioLogo.png deleted file mode 100644 index 9092f582..00000000 Binary files a/Resources/Branding/StarStudioLogo.png and /dev/null differ diff --git a/Sandbox/premake5.lua b/Sandbox/premake5.lua index c7ed5afb..04048cdb 100644 --- a/Sandbox/premake5.lua +++ b/Sandbox/premake5.lua @@ -1,8 +1,5 @@ project "Sandbox" kind "ConsoleApp" - language "C++" - cppdialect "C++17" - staticruntime "off" targetdir ("%{wks.location}/bin/" .. outputdir .. "/%{prj.name}") objdir ("%{wks.location}/bin-int/" .. outputdir .. "/%{prj.name}") @@ -20,16 +17,29 @@ project "Sandbox" "%{wks.location}/StarEngine/vendor", "%{IncludeDir.glm}", "%{IncludeDir.entt}", + "%{IncludeDir.GLFW}", + "%{IncludeDir.Tracy}", "%{IncludeDir.miniaudio}" } links { - "StarEngine" + "StarEngine", + "%{Library.Tracy}", + } + + defines + { + "TRACY_ENABLE", + "TRACY_ON_DEMAND", + "TRACY_CALLSTACK=10", } filter "system:windows" systemversion "latest" + defines { + "SE_PLATFORM_WINDOWS" + } filter "configurations:Debug" defines "SE_DEBUG" @@ -47,4 +57,4 @@ project "Sandbox" optimize "on" filter "action:vs2022" - buildoptions { "/utf-8" } \ No newline at end of file + buildoptions { "/utf-8" } diff --git a/Sandbox/src/ExampleLayer.cpp b/Sandbox/src/ExampleLayer.cpp index be9ffbc5..28b70488 100644 --- a/Sandbox/src/ExampleLayer.cpp +++ b/Sandbox/src/ExampleLayer.cpp @@ -20,7 +20,7 @@ ExampleLayer::ExampleLayer() 0.0f, 0.5f, 0.0f, 0.8f, 0.8f, 0.2f, 1.0f }; - StarEngine::Ref vertexBuffer = StarEngine::VertexBuffer::Create(vertices, sizeof(vertices)); + StarEngine::RefPtr vertexBuffer = StarEngine::VertexBuffer::Create(vertices, sizeof(vertices)); StarEngine::BufferLayout layout = { { StarEngine::ShaderDataType::Float3, "a_Position" }, { StarEngine::ShaderDataType::Float4, "a_Color" } @@ -30,7 +30,7 @@ ExampleLayer::ExampleLayer() m_VertexArray->AddVertexBuffer(vertexBuffer); uint32_t indices[3] = { 0, 1, 2 }; - StarEngine::Ref indexBuffer = StarEngine::IndexBuffer::Create(indices, sizeof(indices) / sizeof(uint32_t)); + StarEngine::RefPtr indexBuffer = StarEngine::IndexBuffer::Create(indices, sizeof(indices) / sizeof(uint32_t)); m_VertexArray->SetIndexBuffer(indexBuffer); m_SquareVA = StarEngine::VertexArray::Create(); @@ -42,7 +42,7 @@ ExampleLayer::ExampleLayer() -0.5f, 0.5f, 0.0f, 0.0f, 1.0f }; - StarEngine::Ref squareVB = StarEngine::VertexBuffer::Create(squareVertices, sizeof(squareVertices)); + StarEngine::RefPtr squareVB = StarEngine::VertexBuffer::Create(squareVertices, sizeof(squareVertices)); squareVB->SetLayout({ { StarEngine::ShaderDataType::Float3, "a_Position" }, { StarEngine::ShaderDataType::Float2, "a_TexCoord" } @@ -51,7 +51,7 @@ ExampleLayer::ExampleLayer() m_SquareVA->AddVertexBuffer(squareVB); uint32_t squareIndices[6] = { 0, 1, 2, 2, 3, 0 }; - StarEngine::Ref squareIB = StarEngine::IndexBuffer::Create(squareIndices, sizeof(squareIndices) / sizeof(uint32_t)); + StarEngine::RefPtr squareIB = StarEngine::IndexBuffer::Create(squareIndices, sizeof(squareIndices) / sizeof(uint32_t)); m_SquareVA->SetIndexBuffer(squareIB); std::string vertexSrc = R"( diff --git a/Sandbox/src/ExampleLayer.h b/Sandbox/src/ExampleLayer.h index 15f7a9bc..77b22a0a 100644 --- a/Sandbox/src/ExampleLayer.h +++ b/Sandbox/src/ExampleLayer.h @@ -18,13 +18,13 @@ class ExampleLayer : public StarEngine::Layer void OnEvent(StarEngine::Event& e) override; private: StarEngine::ShaderLibrary m_ShaderLibrary; - StarEngine::Ref m_Shader; - StarEngine::Ref m_VertexArray; + StarEngine::RefPtr m_Shader; + StarEngine::RefPtr m_VertexArray; - StarEngine::Ref m_FlatColorShader; - StarEngine::Ref m_SquareVA; + StarEngine::RefPtr m_FlatColorShader; + StarEngine::RefPtr m_SquareVA; - StarEngine::Ref m_Texture, m_starLogoTexture; + StarEngine::RefPtr m_Texture, m_starLogoTexture; StarEngine::OrthographicCameraController m_CameraController; glm::vec3 m_SquareColor = { 0.2f, 0.3f, 0.8f }; diff --git a/Sandbox/src/Sandbox2D.cpp b/Sandbox/src/Sandbox2D.cpp index b7a980d4..418cfce6 100644 --- a/Sandbox/src/Sandbox2D.cpp +++ b/Sandbox/src/Sandbox2D.cpp @@ -18,19 +18,19 @@ Sandbox2D::Sandbox2D() void Sandbox2D::OnAttach() { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("Sandbox2D::OnAttach"); //m_CheckerboardTexture = StarEngine::TextureImporter::LoadTexture2D("assets/textures/Checkerboard.png"); } void Sandbox2D::OnDetach() { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("Sandbox2D::OnDetach"); } void Sandbox2D::OnUpdate(StarEngine::Timestep ts) { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("Sandbox2D::OnUpdate"); // Update m_CameraController.OnUpdate(ts); diff --git a/Sandbox/src/Sandbox2D.h b/Sandbox/src/Sandbox2D.h index d63d1558..65940432 100644 --- a/Sandbox/src/Sandbox2D.h +++ b/Sandbox/src/Sandbox2D.h @@ -21,10 +21,10 @@ class Sandbox2D : public StarEngine::Layer bool m_VSync = true; // Temp - StarEngine::Ref m_SquareVA; - StarEngine::Ref m_FlatColorShader; + StarEngine::RefPtr m_SquareVA; + StarEngine::RefPtr m_FlatColorShader; - StarEngine::Ref m_CheckerboardTexture; + StarEngine::RefPtr m_CheckerboardTexture; glm::vec4 m_SquareColor = { 0.2f, 0.3f, 0.8f, 1.0f }; }; diff --git a/StarEditor/Resources/LUA/StarEngine.lua b/StarEditor/Resources/LUA/StarEngine.lua new file mode 100644 index 00000000..153aaf68 --- /dev/null +++ b/StarEditor/Resources/LUA/StarEngine.lua @@ -0,0 +1,51 @@ +local function getAssemblyFiles(directory, is_windows) + if is_windows then + handle = io.popen("dir " .. directory .. " /B /A-D") + else + handle = io.popen("ls " .. directory) + end + + t = {} + for f in handle:lines() do + if path.hasextension(f, ".dll") then + if string.find(f, "System.") then + table.insert(t, f) + end + end + end + + handle:close() + return t +end + +function linkAppReferences(linkScriptCore) + local starengineDir = os.getenv("STARENGINE_DIR") + local monoLibsPath + local monoLibsFacadesPath + local is_windows = os.istarget('windows') + + if is_windows then + monoLibsPath = path.join(starengineDir, "StarEditor", "mono", "lib", "mono", "4.5"):gsub("/", "\\") + monoLibsFacadesPath = path.join(monoLibsPath, "Facades"):gsub("/", "\\") + else + monoLibsPath = path.join(starengineDir, "StarEditor", "mono", "linux", "lib", "mono", "4.5") + monoLibsFacadesPath = path.join(monoLibsPath, "Facades") + end + + -- NOTE(Peter): We HAVE to use libdirs, using the full path in links won't work + -- this is a known issue with Visual Studio... + libdirs { monoLibsPath, monoLibsFacadesPath } + if linkScriptCore ~= false then + links { "StarEngine-ScriptCore" } + end + + for k, v in ipairs(getAssemblyFiles(monoLibsPath, is_windows)) do + --print("Adding reference to: " .. v) + links { v } + end + + for k, v in ipairs(getAssemblyFiles(monoLibsFacadesPath, is_windows)) do + --print("Adding reference to: " .. v) + links { v } + end +end \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/DirShadowMap.glsl b/StarEditor/Resources/Shaders/DirShadowMap.glsl new file mode 100644 index 00000000..e77bc738 --- /dev/null +++ b/StarEditor/Resources/Shaders/DirShadowMap.glsl @@ -0,0 +1,42 @@ +// Shadow Map shader + +#version 450 core +#pragma stage : vert + +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +layout (push_constant) uniform Transform +{ + uint Cascade; +} u_Renderer; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + gl_Position = u_DirShadow.DirLightMatrices[u_Renderer.Cascade] * transform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +void main() +{ + // TODO: Check for alpha in texture +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/DirShadowMap_Anim.glsl b/StarEditor/Resources/Shaders/DirShadowMap_Anim.glsl new file mode 100644 index 00000000..b28088d8 --- /dev/null +++ b/StarEditor/Resources/Shaders/DirShadowMap_Anim.glsl @@ -0,0 +1,53 @@ +// Shadow Map shader + +#version 450 core +#pragma stage : vert + +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +// Bone influences +layout(location = 8) in ivec4 a_BoneIndices; +layout(location = 9) in vec4 a_BoneWeights; + +layout(push_constant) uniform PushConstants +{ + uint Cascade; + uint BoneTransformBaseIndex; + uint BoneTransformStride; +} u_Constants; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + mat4 boneTransform = r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[0]] * a_BoneWeights[0]; + boneTransform += r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[1]] * a_BoneWeights[1]; + boneTransform += r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[2]] * a_BoneWeights[2]; + boneTransform += r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[3]] * a_BoneWeights[3]; + + gl_Position = u_DirShadow.DirLightMatrices[u_Constants.Cascade] * transform * boneTransform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +void main() +{ + // TODO: Check for alpha in texture +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/EnvironmentIrradiance.glsl b/StarEditor/Resources/Shaders/EnvironmentIrradiance.glsl new file mode 100644 index 00000000..13973bd4 --- /dev/null +++ b/StarEditor/Resources/Shaders/EnvironmentIrradiance.glsl @@ -0,0 +1,44 @@ +#version 450 core +#pragma stage : comp + +#include + +// Computes diffuse irradiance cubemap convolution for image-based lighting. +// Uses quasi Monte Carlo sampling with Hammersley sequence. + +layout(binding = 0, rgba32f) restrict writeonly uniform imageCube o_IrradianceMap; +layout(binding = 1) uniform samplerCube u_RadianceMap; + +layout(push_constant) uniform Uniforms +{ + uint Samples; +} u_Uniforms; + +layout(local_size_x=32, local_size_y=32, local_size_z=1) in; +void main() +{ + vec3 N = GetCubeMapTexCoord(vec2(imageSize(o_IrradianceMap))); + + vec3 S, T; + ComputeBasisVectors(N, S, T); + + uint samples = 64 * u_Uniforms.Samples; + + // Monte Carlo integration of hemispherical irradiance. + // As a small optimization this also includes Lambertian BRDF assuming perfectly white surface (albedo of 1.0) + // so we don't need to normalize in PBR fragment shader (so technically it encodes exitant radiance rather than irradiance). + vec3 irradiance = vec3(0); + for(uint i = 0; i < samples; i++) + { + vec2 u = SampleHammersley(i, samples); + vec3 Li = TangentToWorld(SampleHemisphere(u.x, u.y), N, S, T); + float cosTheta = max(0.0, dot(Li, N)); + + // PIs here cancel out because of division by pdf. + vec3 radianceSample = textureLod(u_RadianceMap, Li, 0).rgb; + irradiance += 2.0 * radianceSample * cosTheta; + } + irradiance /= vec3(samples); + + imageStore(o_IrradianceMap, ivec3(gl_GlobalInvocationID), vec4(irradiance, 1.0)); +} diff --git a/StarEditor/Resources/Shaders/EnvironmentMipFilter.glsl b/StarEditor/Resources/Shaders/EnvironmentMipFilter.glsl new file mode 100644 index 00000000..49381bde --- /dev/null +++ b/StarEditor/Resources/Shaders/EnvironmentMipFilter.glsl @@ -0,0 +1,89 @@ +#version 450 core +#pragma stage : comp +#include +#include + +// Pre-filters environment cube map using GGX NDF importance sampling. +// Part of specular IBL split-sum approximation. + + +const uint NumSamples = 1024; +const float InvNumSamples = 1.0 / float(NumSamples); + +vec2 SampleHammersley(uint i) +{ + return vec2(i * InvNumSamples, RadicalInverse_VdC(i)); +} + +const int NumMipLevels = 1; +layout(binding = 0, rgba32f) restrict writeonly uniform imageCube outputTexture[NumMipLevels]; +layout(binding = 1) uniform samplerCube inputTexture; + +layout (push_constant) uniform Uniforms +{ + float Roughness; +} u_Uniforms; + +#define PARAM_LEVEL 0 +#define PARAM_ROUGHNESS u_Uniforms.Roughness + + +layout(local_size_x=32, local_size_y=32, local_size_z=1) in; +void main(void) +{ + // Make sure we won't write past output when computing higher mipmap levels. + ivec2 outputSize = imageSize(outputTexture[PARAM_LEVEL]); + if(gl_GlobalInvocationID.x >= outputSize.x || gl_GlobalInvocationID.y >= outputSize.y) { + return; + } + + // Solid angle associated with a single cubemap texel at zero mipmap level. + // This will come in handy for importance sampling below. + vec2 inputSize = vec2(textureSize(inputTexture, 0)); + float wt = 4.0 * PI / (6 * inputSize.x * inputSize.y); + + // Approximation: Assume zero viewing angle (isotropic reflections). + vec3 N = GetCubeMapTexCoord(vec2(imageSize(outputTexture[PARAM_LEVEL]))); + vec3 Lo = N; + + vec3 S, T; + ComputeBasisVectors(N, S, T); + + vec3 color = vec3(0); + float weight = 0; + + // Convolve environment map using GGX NDF importance sampling. + // Weight by cosine term since Epic claims it generally improves quality. + for(uint i = 0; i < NumSamples; i++) + { + vec2 u = SampleHammersley(i); + vec3 Lh = TangentToWorld(SampleGGX(u.x, u.y, PARAM_ROUGHNESS), N, S, T); + + // Compute incident direction (Li) by reflecting viewing direction (Lo) around half-vector (Lh). + vec3 Li = 2.0 * dot(Lo, Lh) * Lh - Lo; + + float cosLi = dot(N, Li); + if(cosLi > 0.0) { + // Use Mipmap Filtered Importance Sampling to improve convergence. + // See: https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch20.html, section 20.4 + + float cosLh = max(dot(N, Lh), 0.0); + + // GGX normal distribution function (D term) probability density function. + // Scaling by 1/4 is due to change of density in terms of Lh to Li (and since N=V, rest of the scaling factor cancels out). + float pdf = NdfGGX(cosLh, PARAM_ROUGHNESS) * 0.25; + + // Solid angle associated with this sample. + float ws = 1.0 / (NumSamples * pdf); + + // Mip level to sample from. + float mipLevel = max(0.5 * log2(ws / wt) + 1.0, 0.0); + + color += textureLod(inputTexture, Li, mipLevel).rgb * cosLi; + weight += cosLi; + } + } + color /= weight; + + imageStore(outputTexture[PARAM_LEVEL], ivec3(gl_GlobalInvocationID), vec4(color, 1.0)); +} diff --git a/StarEditor/Resources/Shaders/EquirectangularToCubeMap.glsl b/StarEditor/Resources/Shaders/EquirectangularToCubeMap.glsl new file mode 100644 index 00000000..02869704 --- /dev/null +++ b/StarEditor/Resources/Shaders/EquirectangularToCubeMap.glsl @@ -0,0 +1,23 @@ +// Converts equirectangular (lat-long) projection texture into a cubemap +#version 450 core +#pragma stage : comp +#include + +layout(binding = 0, rgba16f) restrict writeonly uniform imageCube o_CubeMap; +layout(binding = 1) uniform sampler2D u_EquirectangularTex; + +layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in; +void main() +{ + vec3 cubeTC = GetCubeMapTexCoord(vec2(imageSize(o_CubeMap))); + + // Calculate sampling coords for equirectangular texture + // https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates + float phi = atan(cubeTC.z, cubeTC.x); + float theta = acos(cubeTC.y); + vec2 uv = vec2(phi / (2.0 * PI) + 0.5, theta / PI); + + vec4 color = texture(u_EquirectangularTex, uv); + color = min(color, vec4(500.0)); + imageStore(o_CubeMap, ivec3(gl_GlobalInvocationID), color); +} diff --git a/StarEditor/Resources/Shaders/FlatColor.glsl b/StarEditor/Resources/Shaders/FlatColor.glsl new file mode 100644 index 00000000..2d39fc5b --- /dev/null +++ b/StarEditor/Resources/Shaders/FlatColor.glsl @@ -0,0 +1,26 @@ +// Flat Color Shader + +#type vertex +#version 330 core + +layout(location = 0) in vec3 a_Position; + +uniform mat4 u_ViewProjection; +uniform mat4 u_Transform; + +void main() +{ + gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0); +} + +#type fragment +#version 330 core + +layout(location = 0) out vec4 color; + +uniform vec4 u_Color; + +void main() +{ + color = u_Color; +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/Grid.glsl b/StarEditor/Resources/Shaders/Grid.glsl new file mode 100644 index 00000000..943f5fc2 --- /dev/null +++ b/StarEditor/Resources/Shaders/Grid.glsl @@ -0,0 +1,51 @@ +// Grid Shader + +#version 450 core +#pragma stage : vert +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +layout(location = 0) out vec2 v_TexCoord; + +layout(push_constant) uniform Transform +{ + mat4 Transform; +} u_Renderer; + +void main() +{ + vec4 position = u_Camera.ViewProjectionMatrix * u_Renderer.Transform * vec4(a_Position, 1.0); + gl_Position = position; + + v_TexCoord = a_TexCoord; +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 color; + +layout(push_constant) uniform Settings +{ + layout(offset = 64) float Scale; + float Size; +} u_Settings; + +layout(location = 0) in vec2 v_TexCoord; + +float grid(vec2 st, float res) +{ + vec2 grid = fract(st); + return step(res, grid.x) * step(res, grid.y); +} + +void main() +{ + float x = grid(v_TexCoord * u_Settings.Scale, u_Settings.Size); + color = vec4(vec3(0.2), 0.5) * (1.0 - x); + + if (color.a == 0.0) + discard; +} diff --git a/StarEditor/Resources/Shaders/HZB.glsl b/StarEditor/Resources/Shaders/HZB.glsl new file mode 100644 index 00000000..221ad779 --- /dev/null +++ b/StarEditor/Resources/Shaders/HZB.glsl @@ -0,0 +1,112 @@ +// --------------------------------------- +// -- Hazel Engine HZB shader -- +// --------------------------------------- +// - Adopted from Unreal's Engine 4 HZB shader + +#version 430 core +#pragma stage : comp + +#define MAX_MIP_BATCH_SIZE 4 +#define LOCAL_SIZE 8 + + +layout(push_constant) uniform Info +{ + vec2 u_DispatchThreadIdToBufferUV; + vec2 u_InputViewportMaxBound; + vec2 u_InvSize; + int u_FirstLod; + bool u_IsFirstPass; +}; + +layout(binding = 0, r32f) writeonly uniform image2D o_HZB[4]; +layout(binding = 1) uniform sampler2D u_InputDepth; +shared float sharedClosestDeviceZ[LOCAL_SIZE * LOCAL_SIZE]; + +vec4 Gather4(sampler2D tex, vec2 bufferUV, float lod) +{ + vec2 uv[4]; + uv[0] = min(bufferUV + vec2(-0.5f, -0.5f) * u_InvSize, u_InputViewportMaxBound); + uv[1] = min(bufferUV + vec2( 0.5f, -0.5f) * u_InvSize, u_InputViewportMaxBound); + uv[2] = min(bufferUV + vec2(-0.5f, 0.5f) * u_InvSize, u_InputViewportMaxBound); + uv[3] = min(bufferUV + vec2( 0.5f, 0.5f) * u_InvSize, u_InputViewportMaxBound); + vec4 depth; + depth.x = textureLod(tex, uv[0], lod).r; + depth.y = textureLod(tex, uv[1], lod).r; + depth.z = textureLod(tex, uv[2], lod).r; + depth.w = textureLod(tex, uv[3], lod).r; + return depth; +} + + +uint SignedRightShift(uint x, const int bitshift) +{ + if (bitshift > 0) + { + return x << uint(bitshift); + } + else if (bitshift < 0) + { + return x >> uint(-bitshift); + } + return x; +} + +ivec2 InitialTilePixelPositionForReduction2x2(const uint tileSizeLog2, uint sharedArrayId) +{ + uint x = 0u; + uint y = 0u; + for (uint i = 0u; i < tileSizeLog2; i++) + { + const uint destBitId = tileSizeLog2 - 1u - i; + const uint destBitMask = 1u << destBitId; + x |= destBitMask & SignedRightShift(sharedArrayId, int(destBitId) - int(i * 2u + 0u)); + y |= destBitMask & SignedRightShift(sharedArrayId, int(destBitId) - int(i * 2u + 1u)); + } + return ivec2(x, y); +} + +layout(local_size_x = LOCAL_SIZE, local_size_y = LOCAL_SIZE, local_size_z = 1) in; +void main() +{ + ivec2 groupThreadId = InitialTilePixelPositionForReduction2x2(uint(MAX_MIP_BATCH_SIZE - 1), gl_LocalInvocationIndex); + ivec2 dispatchThreadId = ivec2(LOCAL_SIZE * gl_WorkGroupID) + groupThreadId; + vec2 bufferUV = (vec2(dispatchThreadId) + vec2(0.5)) * u_DispatchThreadIdToBufferUV.xy; + vec4 deviceZ = Gather4(u_InputDepth, bufferUV, u_FirstLod - 1); + float closestDeviceZ = max(max(deviceZ.x, deviceZ.y), max(deviceZ.z, deviceZ.w)); + ivec2 outputPixelPos = dispatchThreadId; + if(u_IsFirstPass) + { + imageStore(o_HZB[0], outputPixelPos, textureLod(u_InputDepth, bufferUV, 0).xxxx); + } + else + { + imageStore(o_HZB[0], ivec2(outputPixelPos), closestDeviceZ.xxxx); + } + sharedClosestDeviceZ[gl_LocalInvocationIndex] = closestDeviceZ; + + barrier(); + + for (int mipLevel = 1; mipLevel < MAX_MIP_BATCH_SIZE; mipLevel++) + { + const int tileSize = LOCAL_SIZE / (1 << mipLevel); + const int reduceBankSize = tileSize * tileSize; + + if (gl_LocalInvocationIndex < reduceBankSize) + { + vec4 parentDeviceZ; + parentDeviceZ.x = closestDeviceZ; + for (uint i = 1u; i < MAX_MIP_BATCH_SIZE; i++) + { + // LDS index + parentDeviceZ[i] = sharedClosestDeviceZ[gl_LocalInvocationIndex + i * reduceBankSize]; + } + closestDeviceZ = max(max(parentDeviceZ.x, parentDeviceZ.y), max(parentDeviceZ.z, parentDeviceZ.w)); + outputPixelPos = outputPixelPos >> ivec2(1); + sharedClosestDeviceZ[gl_LocalInvocationIndex] = closestDeviceZ; + imageStore(o_HZB[mipLevel], outputPixelPos, closestDeviceZ.xxxx); + } + } +} + + diff --git a/StarEditor/Resources/Shaders/HazelPBR_Anim.glsl b/StarEditor/Resources/Shaders/HazelPBR_Anim.glsl new file mode 100644 index 00000000..128f411a --- /dev/null +++ b/StarEditor/Resources/Shaders/HazelPBR_Anim.glsl @@ -0,0 +1,350 @@ +/*// -- Hazel Engine PBR shader -- +// ----------------------------- +// Note: this shader is still very much in progress. There are likely many bugs and future additions that will go in. +// Currently heavily updated. +// +// References upon which this is based: +// - Unreal Engine 4 PBR notes (https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf) +// - Frostbite's SIGGRAPH 2014 paper (https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/) +// - Michał Siejak's PBR project (https://github.com/Nadrin) +// - My implementation from years ago in the Sparky engine (https://github.com/TheCherno/Sparky) +*/ +#version 450 core +#pragma stage:vert + +#include +#include +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +// Bone influences +layout(location = 8) in ivec4 a_BoneIndices; +layout(location = 9) in vec4 a_BoneWeights; + +layout(push_constant) uniform BoneTransformIndex +{ + uint Base; + uint Stride; +} u_BoneTransformIndex; + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) out VertexOutput Output; + +// Make sure both shaders compute the exact same answer(PreDepth). +// We need to have the same exact calculations to produce the gl_Position value (eg. matrix multiplications). +invariant gl_Position; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + mat4 boneTransform = r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[0]] * a_BoneWeights[0]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[1]] * a_BoneWeights[1]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[2]] * a_BoneWeights[2]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[3]] * a_BoneWeights[3]; + + vec4 worldPosition = transform * boneTransform * vec4(a_Position, 1.0); + + Output.WorldPosition = worldPosition.xyz; + Output.Normal = mat3(transform) * mat3(boneTransform) * a_Normal; + Output.TexCoord = vec2(a_TexCoord.x, 1.0 - a_TexCoord.y); + Output.WorldNormals = mat3(transform) * mat3(boneTransform) * mat3(a_Tangent, a_Binormal, a_Normal); + Output.WorldTransform = mat3(transform) * mat3(boneTransform); + Output.Binormal = mat3(transform) * mat3(boneTransform) * a_Binormal; + + Output.CameraView = mat3(u_Camera.ViewMatrix); + + vec4 shadowCoords[4]; + shadowCoords[0] = u_DirShadow.DirLightMatrices[0] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[1] = u_DirShadow.DirLightMatrices[1] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[2] = u_DirShadow.DirLightMatrices[2] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[3] = u_DirShadow.DirLightMatrices[3] * vec4(Output.WorldPosition.xyz, 1.0); + Output.ShadowMapCoords[0] = vec3(shadowCoords[0].xyz / shadowCoords[0].w); + Output.ShadowMapCoords[1] = vec3(shadowCoords[1].xyz / shadowCoords[1].w); + Output.ShadowMapCoords[2] = vec3(shadowCoords[2].xyz / shadowCoords[2].w); + Output.ShadowMapCoords[3] = vec3(shadowCoords[3].xyz / shadowCoords[3].w); + + Output.ViewPosition = vec3(u_Camera.ViewMatrix * vec4(Output.WorldPosition, 1.0)); + + gl_Position = u_Camera.ViewProjectionMatrix * worldPosition; +} + + +#version 450 core + +#pragma stage : frag + +#include +#include +#include +#include +#include + +// Constant normal incidence Fresnel factor for all dielectrics. +const vec3 Fdielectric = vec3(0.04); + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) in VertexOutput Input; + +layout(location = 0) out vec4 color; +layout(location = 1) out vec4 o_ViewNormalsLuminance; +layout(location = 2) out vec4 o_MetalnessRoughness; + +layout(push_constant) uniform Material +{ + layout(offset = 16) vec3 AlbedoColor; + float Metalness; + float Roughness; + float Emission; + + float EnvMapRotation; + + bool UseNormalMap; +} u_MaterialUniforms; + +vec3 IBL(vec3 F0, vec3 Lr) +{ + vec3 irradiance = texture(u_EnvIrradianceTex, m_Params.Normal).rgb; + vec3 F = FresnelSchlickRoughness(F0, m_Params.NdotV, m_Params.Roughness); + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseIBL = m_Params.Albedo * irradiance; + + int envRadianceTexLevels = textureQueryLevels(u_EnvRadianceTex); + vec3 specularIrradiance = textureLod(u_EnvRadianceTex, RotateVectorAboutY(u_MaterialUniforms.EnvMapRotation, Lr), m_Params.Roughness * envRadianceTexLevels).rgb; + + vec2 specularBRDF = texture(u_BRDFLUTTexture, vec2(m_Params.NdotV, m_Params.Roughness)).rg; + vec3 specularIBL = specularIrradiance * (F0 * specularBRDF.x + specularBRDF.y); + + return kd * diffuseIBL + specularIBL; +} + + +///////////////////////////////////////////// + +vec3 GetGradient(float value) +{ + vec3 zero = vec3(0.0, 0.0, 0.0); + vec3 white = vec3(0.0, 0.1, 0.9); + vec3 red = vec3(0.2, 0.9, 0.4); + vec3 blue = vec3(0.8, 0.8, 0.3); + vec3 green = vec3(0.9, 0.2, 0.3); + + float step0 = 0.0f; + float step1 = 2.0f; + float step2 = 4.0f; + float step3 = 8.0f; + float step4 = 16.0f; + + vec3 color = mix(zero, white, smoothstep(step0, step1, value)); + color = mix(color, white, smoothstep(step1, step2, value)); + color = mix(color, red, smoothstep(step1, step2, value)); + color = mix(color, blue, smoothstep(step2, step3, value)); + color = mix(color, green, smoothstep(step3, step4, value)); + + return color; +} + + +void main() +{ + // Standard PBR inputs + vec4 albedoTexColor = texture(u_AlbedoTexture, Input.TexCoord); + m_Params.Albedo = albedoTexColor.rgb * ToLinear(vec4(u_MaterialUniforms.AlbedoColor, 1.0)).rgb; // MaterialUniforms.AlbedoColor is perceptual, must be converted to linear. + float alpha = albedoTexColor.a; + // note: Metalness and roughness could be in the same texture. + // Per GLTF spec, we read metalness from the B channel and roughness from the G channel + // This will still work if metalness and roughness are independent greyscale textures, + // but it will not work if metalness and roughness are independent textures containing only R channel. + m_Params.Metalness = texture(u_MetalnessTexture, Input.TexCoord).b * u_MaterialUniforms.Metalness; + m_Params.Roughness = texture(u_RoughnessTexture, Input.TexCoord).g * u_MaterialUniforms.Roughness; + o_MetalnessRoughness = vec4(m_Params.Metalness, m_Params.Roughness, 0.f, 1.f); + m_Params.Roughness = max(m_Params.Roughness, 0.05); // Minimum roughness of 0.05 to keep specular highlight + + // Normals (either from vertex or map) + m_Params.Normal = normalize(Input.Normal); + if (u_MaterialUniforms.UseNormalMap) + { + m_Params.Normal = normalize(texture(u_NormalTexture, Input.TexCoord).rgb * 2.0f - 1.0f); + m_Params.Normal = normalize(Input.WorldNormals * m_Params.Normal); + } + // View normals + o_ViewNormalsLuminance.xyz = Input.CameraView * m_Params.Normal; + + m_Params.View = normalize(u_Scene.CameraPosition - Input.WorldPosition); + m_Params.NdotV = max(dot(m_Params.Normal, m_Params.View), 0.0); + + // Specular reflection vector + vec3 Lr = 2.0 * m_Params.NdotV * m_Params.Normal - m_Params.View; + + // Fresnel reflectance, metals use albedo + vec3 F0 = mix(Fdielectric, m_Params.Albedo, m_Params.Metalness); + + uint cascadeIndex = 0; + + const uint SHADOW_MAP_CASCADE_COUNT = 4; + for (uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; i++) + { + if (Input.ViewPosition.z < u_RendererData.CascadeSplits[i]) + cascadeIndex = i + 1; + } + + float shadowDistance = u_RendererData.MaxShadowDistance;//u_CascadeSplits[3]; + float transitionDistance = u_RendererData.ShadowFade; + float distance = length(Input.ViewPosition); + ShadowFade = distance - (shadowDistance - transitionDistance); + ShadowFade /= transitionDistance; + ShadowFade = clamp(1.0 - ShadowFade, 0.0, 1.0); + + float shadowScale; + + bool fadeCascades = u_RendererData.CascadeFading; + if (fadeCascades) + { + float cascadeTransitionFade = u_RendererData.CascadeTransitionFade; + + float c0 = smoothstep(u_RendererData.CascadeSplits[0] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[0] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + float c1 = smoothstep(u_RendererData.CascadeSplits[1] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[1] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + float c2 = smoothstep(u_RendererData.CascadeSplits[2] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[2] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + if (c0 > 0.0 && c0 < 1.0) + { + // Sample 0 & 1 + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 0); + float shadowAmount0 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 0, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 0, shadowMapCoords); + shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 1); + float shadowAmount1 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords); + + shadowScale = mix(shadowAmount0, shadowAmount1, c0); + } + else if (c1 > 0.0 && c1 < 1.0) + { + // Sample 1 & 2 + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 1); + float shadowAmount1 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords); + shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 2); + float shadowAmount2 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords); + + shadowScale = mix(shadowAmount1, shadowAmount2, c1); + } + else if (c2 > 0.0 && c2 < 1.0) + { + // Sample 2 & 3 + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 2); + float shadowAmount2 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords); + shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 3); + float shadowAmount3 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 3, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 3, shadowMapCoords); + + shadowScale = mix(shadowAmount2, shadowAmount3, c2); + } + else + { + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex); + shadowScale = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords); + } + } + else + { + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex); + shadowScale = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords); + } + + shadowScale = 1.0 - clamp(u_Scene.DirectionalLights.ShadowAmount - shadowScale, 0.0f, 1.0f); + + // Direct lighting + vec3 lightContribution = CalculateDirLights(F0) * shadowScale; + lightContribution += CalculatePointLights(F0, Input.WorldPosition); + lightContribution += CalculateSpotLights(F0, Input.WorldPosition) * SpotShadowCalculation(u_SpotShadowTexture, Input.WorldPosition); + lightContribution += m_Params.Albedo * u_MaterialUniforms.Emission; + + // Indirect lighting + vec3 iblContribution = IBL(F0, Lr) * u_Scene.EnvironmentMapIntensity; + + // Final color + color = vec4(iblContribution + lightContribution, 1.0); + + // TODO: Temporary bug fix. + if (u_Scene.DirectionalLights.Multiplier <= 0.0f) + shadowScale = 0.0f; + + // Shadow mask with respect to bright surfaces. + o_ViewNormalsLuminance.a = clamp(shadowScale + dot(color.rgb, vec3(0.2125f, 0.7154f, 0.0721f)), 0.0f, 1.0f); + + if (u_RendererData.ShowLightComplexity) + { + int pointLightCount = GetPointLightCount(); + int spotLightCount = GetSpotLightCount(); + + float value = float(pointLightCount + spotLightCount); + color.rgb = (color.rgb * 0.2) + GetGradient(value); + } + + // TODO(Karim): Have a separate render pass for translucent and transparent objects. + // Because we use the pre-depth image for depth test. + // color.a = alpha; + + // (shading-only) + // color.rgb = vec3(1.0) * shadowScale + 0.2f; + + if (u_RendererData.ShowCascades) + { + switch (cascadeIndex) + { + case 0: + color.rgb *= vec3(1.0f, 0.25f, 0.25f); + break; + case 1: + color.rgb *= vec3(0.25f, 1.0f, 0.25f); + break; + case 2: + color.rgb *= vec3(0.25f, 0.25f, 1.0f); + break; + case 3: + color.rgb *= vec3(1.0f, 1.0f, 0.25f); + break; + } + } +} + + diff --git a/StarEditor/Resources/Shaders/HazelPBR_Static.glsl b/StarEditor/Resources/Shaders/HazelPBR_Static.glsl new file mode 100644 index 00000000..37f852e9 --- /dev/null +++ b/StarEditor/Resources/Shaders/HazelPBR_Static.glsl @@ -0,0 +1,334 @@ +/*// -- Hazel Engine PBR shader -- +// ----------------------------- +// Note: this shader is still very much in progress. There are likely many bugs and future additions that will go in. +// Currently heavily updated. +// +// References upon which this is based: +// - Unreal Engine 4 PBR notes (https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf) +// - Frostbite's SIGGRAPH 2014 paper (https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/) +// - Michał Siejak's PBR project (https://github.com/Nadrin) +// - My implementation from years ago in the Sparky engine (https://github.com/TheCherno/Sparky) +*/ +#version 450 core +#pragma stage:vert + +#include +#include +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) out VertexOutput Output; + +// Make sure both shaders compute the exact same answer(PreDepth). +// We need to have the same exact calculations to produce the gl_Position value (eg. matrix multiplications). +invariant gl_Position; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + vec4 worldPosition = transform * vec4(a_Position, 1.0); + + Output.WorldPosition = worldPosition.xyz; + Output.Normal = mat3(transform) * a_Normal; + Output.TexCoord = vec2(a_TexCoord.x, 1.0 - a_TexCoord.y); + Output.WorldNormals = mat3(transform) * mat3(a_Tangent, a_Binormal, a_Normal); + Output.WorldTransform = mat3(transform); + Output.Binormal = a_Binormal; + + Output.CameraView = mat3(u_Camera.ViewMatrix); + + vec4 shadowCoords[4]; + shadowCoords[0] = u_DirShadow.DirLightMatrices[0] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[1] = u_DirShadow.DirLightMatrices[1] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[2] = u_DirShadow.DirLightMatrices[2] * vec4(Output.WorldPosition.xyz, 1.0); + shadowCoords[3] = u_DirShadow.DirLightMatrices[3] * vec4(Output.WorldPosition.xyz, 1.0); + Output.ShadowMapCoords[0] = vec3(shadowCoords[0].xyz / shadowCoords[0].w); + Output.ShadowMapCoords[1] = vec3(shadowCoords[1].xyz / shadowCoords[1].w); + Output.ShadowMapCoords[2] = vec3(shadowCoords[2].xyz / shadowCoords[2].w); + Output.ShadowMapCoords[3] = vec3(shadowCoords[3].xyz / shadowCoords[3].w); + + Output.ViewPosition = vec3(u_Camera.ViewMatrix * vec4(Output.WorldPosition, 1.0)); + + gl_Position = u_Camera.ViewProjectionMatrix * worldPosition; +} + + +#version 450 core + +#pragma stage : frag + +#include +#include +#include +#include +#include + +// Constant normal incidence Fresnel factor for all dielectrics. +const vec3 Fdielectric = vec3(0.04); + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec3 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) in VertexOutput Input; + +layout(location = 0) out vec4 color; +layout(location = 1) out vec4 o_ViewNormalsLuminance; +layout(location = 2) out vec4 o_MetalnessRoughness; + +layout(push_constant) uniform Material +{ + vec3 AlbedoColor; + float Metalness; + float Roughness; + float Emission; + + float EnvMapRotation; + + bool UseNormalMap; +} u_MaterialUniforms; + +vec3 IBL(vec3 F0, vec3 Lr) +{ + vec3 irradiance = texture(u_EnvIrradianceTex, m_Params.Normal).rgb; + vec3 F = FresnelSchlickRoughness(F0, m_Params.NdotV, m_Params.Roughness); + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseIBL = m_Params.Albedo * irradiance; + + int envRadianceTexLevels = textureQueryLevels(u_EnvRadianceTex); + vec3 specularIrradiance = textureLod(u_EnvRadianceTex, RotateVectorAboutY(u_MaterialUniforms.EnvMapRotation, Lr), m_Params.Roughness * envRadianceTexLevels).rgb; + + vec2 specularBRDF = texture(u_BRDFLUTTexture, vec2(m_Params.NdotV, m_Params.Roughness)).rg; + vec3 specularIBL = specularIrradiance * (F0 * specularBRDF.x + specularBRDF.y); + + return kd * diffuseIBL + specularIBL; +} + + +///////////////////////////////////////////// + +vec3 GetGradient(float value) +{ + vec3 zero = vec3(0.0, 0.0, 0.0); + vec3 white = vec3(0.0, 0.1, 0.9); + vec3 red = vec3(0.2, 0.9, 0.4); + vec3 blue = vec3(0.8, 0.8, 0.3); + vec3 green = vec3(0.9, 0.2, 0.3); + + float step0 = 0.0f; + float step1 = 2.0f; + float step2 = 4.0f; + float step3 = 8.0f; + float step4 = 16.0f; + + vec3 color = mix(zero, white, smoothstep(step0, step1, value)); + color = mix(color, white, smoothstep(step1, step2, value)); + color = mix(color, red, smoothstep(step1, step2, value)); + color = mix(color, blue, smoothstep(step2, step3, value)); + color = mix(color, green, smoothstep(step3, step4, value)); + + return color; +} + + +void main() +{ + // Standard PBR inputs + vec4 albedoTexColor = texture(u_AlbedoTexture, Input.TexCoord); + m_Params.Albedo = albedoTexColor.rgb * ToLinear(vec4(u_MaterialUniforms.AlbedoColor, 1.0)).rgb; // MaterialUniforms.AlbedoColor is perceptual, must be converted to linear. + float alpha = albedoTexColor.a; + // note: Metalness and roughness could be in the same texture. + // Per GLTF spec, we read metalness from the B channel and roughness from the G channel + // This will still work if metalness and roughness are independent greyscale textures, + // but it will not work if metalness and roughness are independent textures containing only R channel. + m_Params.Metalness = texture(u_MetalnessTexture, Input.TexCoord).b * u_MaterialUniforms.Metalness; + m_Params.Roughness = texture(u_RoughnessTexture, Input.TexCoord).g * u_MaterialUniforms.Roughness; + o_MetalnessRoughness = vec4(m_Params.Metalness, m_Params.Roughness, 0.f, 1.f); + m_Params.Roughness = max(m_Params.Roughness, 0.05); // Minimum roughness of 0.05 to keep specular highlight + + // Normals (either from vertex or map) + m_Params.Normal = normalize(Input.Normal); + if (u_MaterialUniforms.UseNormalMap) + { + m_Params.Normal = normalize(texture(u_NormalTexture, Input.TexCoord).rgb * 2.0f - 1.0f); + m_Params.Normal = normalize(Input.WorldNormals * m_Params.Normal); + } + // View normals + o_ViewNormalsLuminance.xyz = Input.CameraView * m_Params.Normal; + + m_Params.View = normalize(u_Scene.CameraPosition - Input.WorldPosition); + m_Params.NdotV = max(dot(m_Params.Normal, m_Params.View), 0.0); + + // Specular reflection vector + vec3 Lr = 2.0 * m_Params.NdotV * m_Params.Normal - m_Params.View; + + // Fresnel reflectance, metals use albedo + vec3 F0 = mix(Fdielectric, m_Params.Albedo, m_Params.Metalness); + + uint cascadeIndex = 0; + + const uint SHADOW_MAP_CASCADE_COUNT = 4; + for (uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; i++) + { + if (Input.ViewPosition.z < u_RendererData.CascadeSplits[i]) + cascadeIndex = i + 1; + } + + float shadowDistance = u_RendererData.MaxShadowDistance;//u_CascadeSplits[3]; + float transitionDistance = u_RendererData.ShadowFade; + float distance = length(Input.ViewPosition); + ShadowFade = distance - (shadowDistance - transitionDistance); + ShadowFade /= transitionDistance; + ShadowFade = clamp(1.0 - ShadowFade, 0.0, 1.0); + + float shadowScale; + + bool fadeCascades = u_RendererData.CascadeFading; + if (fadeCascades) + { + float cascadeTransitionFade = u_RendererData.CascadeTransitionFade; + + float c0 = smoothstep(u_RendererData.CascadeSplits[0] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[0] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + float c1 = smoothstep(u_RendererData.CascadeSplits[1] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[1] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + float c2 = smoothstep(u_RendererData.CascadeSplits[2] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[2] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + if (c0 > 0.0 && c0 < 1.0) + { + // Sample 0 & 1 + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 0); + float shadowAmount0 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 0, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 0, shadowMapCoords); + shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 1); + float shadowAmount1 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords); + + shadowScale = mix(shadowAmount0, shadowAmount1, c0); + } + else if (c1 > 0.0 && c1 < 1.0) + { + // Sample 1 & 2 + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 1); + float shadowAmount1 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords); + shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 2); + float shadowAmount2 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords); + + shadowScale = mix(shadowAmount1, shadowAmount2, c1); + } + else if (c2 > 0.0 && c2 < 1.0) + { + // Sample 2 & 3 + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 2); + float shadowAmount2 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords); + shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, 3); + float shadowAmount3 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 3, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 3, shadowMapCoords); + + shadowScale = mix(shadowAmount2, shadowAmount3, c2); + } + else + { + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex); + shadowScale = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords); + } + } + else + { + vec3 shadowMapCoords = GetShadowMapCoords(Input.ShadowMapCoords, cascadeIndex); + shadowScale = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords); + } + + shadowScale = 1.0 - clamp(u_Scene.DirectionalLights.ShadowAmount - shadowScale, 0.0f, 1.0f); + + // Direct lighting + vec3 lightContribution = CalculateDirLights(F0) * shadowScale; + lightContribution += CalculatePointLights(F0, Input.WorldPosition); + lightContribution += CalculateSpotLights(F0, Input.WorldPosition) * SpotShadowCalculation(u_SpotShadowTexture, Input.WorldPosition); + lightContribution += m_Params.Albedo * u_MaterialUniforms.Emission; + + // Indirect lighting + vec3 iblContribution = IBL(F0, Lr) * u_Scene.EnvironmentMapIntensity; + + // Final color + color = vec4(iblContribution + lightContribution, 1.0); + + // TODO: Temporary bug fix. + if (u_Scene.DirectionalLights.Multiplier <= 0.0f) + shadowScale = 0.0f; + + // Shadow mask with respect to bright surfaces. + o_ViewNormalsLuminance.a = clamp(shadowScale + dot(color.rgb, vec3(0.2125f, 0.7154f, 0.0721f)), 0.0f, 1.0f); + + if (u_RendererData.ShowLightComplexity) + { + int pointLightCount = GetPointLightCount(); + int spotLightCount = GetSpotLightCount(); + + float value = float(pointLightCount + spotLightCount); + color.rgb = (color.rgb * 0.2) + GetGradient(value); + } + + // TODO(Karim): Have a separate render pass for translucent and transparent objects. + // Because we use the pre-depth image for depth test. + // color.a = alpha; + + // (shading-only) + // color.rgb = vec3(1.0) * shadowScale + 0.2f; + + if (u_RendererData.ShowCascades) + { + switch (cascadeIndex) + { + case 0: + color.rgb *= vec3(1.0f, 0.25f, 0.25f); + break; + case 1: + color.rgb *= vec3(0.25f, 1.0f, 0.25f); + break; + case 2: + color.rgb *= vec3(0.25f, 0.25f, 1.0f); + break; + case 3: + color.rgb *= vec3(1.0f, 1.0f, 0.25f); + break; + } + } +} + + diff --git a/StarEditor/Resources/Shaders/HazelPBR_Transparent.glsl b/StarEditor/Resources/Shaders/HazelPBR_Transparent.glsl new file mode 100644 index 00000000..663a5589 --- /dev/null +++ b/StarEditor/Resources/Shaders/HazelPBR_Transparent.glsl @@ -0,0 +1,318 @@ +/*// -- Hazel Engine PBR shader -- +// ----------------------------- +// Note: this shader is still very much in progress. There are likely many bugs and future additions that will go in. +// Currently heavily updated. +// +// References upon which this is based: +// - Unreal Engine 4 PBR notes (https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf) +// - Frostbite's SIGGRAPH 2014 paper (https://seblagarde.wordpress.com/2015/07/14/siggraph-2014-moving-frostbite-to-physically-based-rendering/) +// - Michał Siejak's PBR project (https://github.com/Nadrin) +// - My implementation from years ago in the Sparky engine (https://github.com/TheCherno/Sparky) +*/ +#version 450 core +#pragma stage:vert + +#include +#include +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec4 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) out VertexOutput Output; + +// Make sure both shaders compute the exact same answer(PreDepth). +// We need to have the same exact calculations to produce the gl_Position value (eg. matrix multiplications). +invariant gl_Position; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + vec4 worldPosition = transform * vec4(a_Position, 1.0); + + Output.WorldPosition = worldPosition.xyz; + Output.Normal = mat3(transform) * a_Normal; + Output.TexCoord = vec2(a_TexCoord.x, 1.0 - a_TexCoord.y); + Output.WorldNormals = mat3(transform) * mat3(a_Tangent, a_Binormal, a_Normal); + Output.WorldTransform = mat3(transform); + Output.Binormal = a_Binormal; + + Output.CameraView = mat3(u_Camera.ViewMatrix); + + Output.ShadowMapCoords[0] = u_DirShadow.DirLightMatrices[0] * vec4(Output.WorldPosition, 1.0); + Output.ShadowMapCoords[1] = u_DirShadow.DirLightMatrices[1] * vec4(Output.WorldPosition, 1.0); + Output.ShadowMapCoords[2] = u_DirShadow.DirLightMatrices[2] * vec4(Output.WorldPosition, 1.0); + Output.ShadowMapCoords[3] = u_DirShadow.DirLightMatrices[3] * vec4(Output.WorldPosition, 1.0); + Output.ViewPosition = vec3(u_Camera.ViewMatrix * vec4(Output.WorldPosition, 1.0)); + + gl_Position = u_Camera.ViewProjectionMatrix * worldPosition; +} + + +#version 450 core + +#pragma stage : frag + +#include +#include +#include +#include +#include + +// Constant normal incidence Fresnel factor for all dielectrics. +const vec3 Fdielectric = vec3(0.04); + +struct VertexOutput +{ + vec3 WorldPosition; + vec3 Normal; + vec2 TexCoord; + mat3 WorldNormals; + mat3 WorldTransform; + vec3 Binormal; + + mat3 CameraView; + + vec4 ShadowMapCoords[4]; + vec3 ViewPosition; +}; + +layout(location = 0) in VertexOutput Input; + +layout(location = 0) out vec4 color; +layout(location = 1) out vec4 o_ViewNormalsLuminance; +layout(location = 2) out vec4 o_MetalnessRoughness; + +layout(push_constant) uniform Material +{ + vec3 AlbedoColor; + float Transparency; + float Roughness; + float Emission; + + float EnvMapRotation; + + bool UseNormalMap; +} u_MaterialUniforms; + +vec3 IBL(vec3 F0, vec3 Lr) +{ + vec3 irradiance = texture(u_EnvIrradianceTex, m_Params.Normal).rgb; + vec3 F = FresnelSchlickRoughness(F0, m_Params.NdotV, m_Params.Roughness); + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseIBL = m_Params.Albedo * irradiance; + + int envRadianceTexLevels = textureQueryLevels(u_EnvRadianceTex); + vec3 specularIrradiance = textureLod(u_EnvRadianceTex, RotateVectorAboutY(u_MaterialUniforms.EnvMapRotation, Lr), m_Params.Roughness * envRadianceTexLevels).rgb; + + vec2 specularBRDF = texture(u_BRDFLUTTexture, vec2(m_Params.NdotV, m_Params.Roughness)).rg; + vec3 specularIBL = specularIrradiance * (F0 * specularBRDF.x + specularBRDF.y); + + return kd * diffuseIBL + specularIBL; +} + + +///////////////////////////////////////////// + +vec3 GetGradient(float value) +{ + vec3 zero = vec3(0.0, 0.0, 0.0); + vec3 white = vec3(0.0, 0.1, 0.9); + vec3 red = vec3(0.2, 0.9, 0.4); + vec3 blue = vec3(0.8, 0.8, 0.3); + vec3 green = vec3(0.9, 0.2, 0.3); + + float step0 = 0.0f; + float step1 = 2.0f; + float step2 = 4.0f; + float step3 = 8.0f; + float step4 = 16.0f; + + vec3 color = mix(zero, white, smoothstep(step0, step1, value)); + color = mix(color, white, smoothstep(step1, step2, value)); + color = mix(color, red, smoothstep(step1, step2, value)); + color = mix(color, blue, smoothstep(step2, step3, value)); + color = mix(color, green, smoothstep(step3, step4, value)); + + return color; +} + + +void main() +{ + // Standard PBR inputs + vec4 albedoTexColor = texture(u_AlbedoTexture, Input.TexCoord); + m_Params.Albedo = albedoTexColor.rgb * ToLinear(vec4(u_MaterialUniforms.AlbedoColor, 1.0)).rgb; // MaterialUniforms.AlbedoColor is perceptual, must be converted to linear. + float alpha = albedoTexColor.a; + m_Params.Metalness = 0.0f; + m_Params.Roughness = 0.0f; + m_Params.Roughness = max(m_Params.Roughness, 0.05); // Minimum roughness of 0.05 to keep specular highlight + + // Normals (either from vertex or map) + m_Params.Normal = normalize(Input.Normal); + + // View normals + //o_ViewNormalsLuminance.xyz = vec3(0.0); + + m_Params.View = normalize(u_Scene.CameraPosition - Input.WorldPosition); + m_Params.NdotV = max(dot(m_Params.Normal, m_Params.View), 0.0); + + // Specular reflection vector + vec3 Lr = 2.0 * m_Params.NdotV * m_Params.Normal - m_Params.View; + + // Fresnel reflectance, metals use albedo + vec3 F0 = mix(Fdielectric, m_Params.Albedo, m_Params.Metalness); + + uint cascadeIndex = 0; + + const uint SHADOW_MAP_CASCADE_COUNT = 4; + for (uint i = 0; i < SHADOW_MAP_CASCADE_COUNT - 1; i++) + { + if (Input.ViewPosition.z < u_RendererData.CascadeSplits[i]) + cascadeIndex = i + 1; + } + + float shadowDistance = u_RendererData.MaxShadowDistance;//u_CascadeSplits[3]; + float transitionDistance = u_RendererData.ShadowFade; + float distance = length(Input.ViewPosition); + ShadowFade = distance - (shadowDistance - transitionDistance); + ShadowFade /= transitionDistance; + ShadowFade = clamp(1.0 - ShadowFade, 0.0, 1.0); + + float shadowScale; + + bool fadeCascades = u_RendererData.CascadeFading; + if (fadeCascades) + { + float cascadeTransitionFade = u_RendererData.CascadeTransitionFade; + + float c0 = smoothstep(u_RendererData.CascadeSplits[0] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[0] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + float c1 = smoothstep(u_RendererData.CascadeSplits[1] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[1] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + float c2 = smoothstep(u_RendererData.CascadeSplits[2] + cascadeTransitionFade * 0.5f, u_RendererData.CascadeSplits[2] - cascadeTransitionFade * 0.5f, Input.ViewPosition.z); + if (c0 > 0.0 && c0 < 1.0) + { + // Sample 0 & 1 + vec3 shadowMapCoords = (Input.ShadowMapCoords[0].xyz / Input.ShadowMapCoords[0].w); + float shadowAmount0 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 0, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 0, shadowMapCoords); + shadowMapCoords = (Input.ShadowMapCoords[1].xyz / Input.ShadowMapCoords[1].w); + float shadowAmount1 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords); + + shadowScale = mix(shadowAmount0, shadowAmount1, c0); + } + else if (c1 > 0.0 && c1 < 1.0) + { + // Sample 1 & 2 + vec3 shadowMapCoords = (Input.ShadowMapCoords[1].xyz / Input.ShadowMapCoords[1].w); + float shadowAmount1 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 1, shadowMapCoords); + shadowMapCoords = (Input.ShadowMapCoords[2].xyz / Input.ShadowMapCoords[2].w); + float shadowAmount2 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords); + + shadowScale = mix(shadowAmount1, shadowAmount2, c1); + } + else if (c2 > 0.0 && c2 < 1.0) + { + // Sample 2 & 3 + vec3 shadowMapCoords = (Input.ShadowMapCoords[2].xyz / Input.ShadowMapCoords[2].w); + float shadowAmount2 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 2, shadowMapCoords); + shadowMapCoords = (Input.ShadowMapCoords[3].xyz / Input.ShadowMapCoords[3].w); + float shadowAmount3 = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, 3, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, 3, shadowMapCoords); + + shadowScale = mix(shadowAmount2, shadowAmount3, c2); + } + else + { + vec3 shadowMapCoords = (Input.ShadowMapCoords[cascadeIndex].xyz / Input.ShadowMapCoords[cascadeIndex].w); + shadowScale = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords); + } + } + else + { + vec3 shadowMapCoords = (Input.ShadowMapCoords[cascadeIndex].xyz / Input.ShadowMapCoords[cascadeIndex].w); + shadowScale = u_RendererData.SoftShadows ? PCSS_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords, u_RendererData.LightSize) : HardShadows_DirectionalLight(u_ShadowMapTexture, cascadeIndex, shadowMapCoords); + } + + shadowScale = 1.0 - clamp(u_Scene.DirectionalLights.ShadowAmount - shadowScale, 0.0f, 1.0f); + + vec3 lightContribution = CalculateDirLights(F0) * shadowScale; + lightContribution += CalculatePointLights(F0, Input.WorldPosition); + lightContribution += CalculateSpotLights(F0, Input.WorldPosition) * SpotShadowCalculation(u_SpotShadowTexture, Input.WorldPosition); + lightContribution += m_Params.Albedo * u_MaterialUniforms.Emission; + + // Indirect lighting + vec3 iblContribution = IBL(F0, Lr) * u_Scene.EnvironmentMapIntensity; + + //color = vec4(iblContribution + lightContribution, 1.0); + color = vec4(m_Params.Albedo, u_MaterialUniforms.Transparency); + + // TODO: Temporary bug fix. + if (u_Scene.DirectionalLights.Multiplier <= 0.0f) + shadowScale = 0.0f; + + // Shadow mask with respect to bright surfaces. + //o_ViewNormalsLuminance.a = clamp(shadowScale + dot(color.rgb, vec3(0.2125f, 0.7154f, 0.0721f)), 0.0f, 1.0f); + + if (u_RendererData.ShowLightComplexity) + { + int pointLightCount = GetPointLightCount(); + int spotLightCount = GetSpotLightCount(); + + float value = float(pointLightCount + spotLightCount); + color.rgb = (color.rgb * 0.2) + GetGradient(value); + } + + // TODO(Karim): Have a separate render pass for translucent and transparent objects. + // Because we use the pre-depth image for depth test. + // color.a = alpha; + + // (shading-only) + // color.rgb = vec3(1.0) * shadowScale + 0.2f; + + if (u_RendererData.ShowCascades) + { + switch (cascadeIndex) + { + case 0: + color.rgb *= vec3(1.0f, 0.25f, 0.25f); + break; + case 1: + color.rgb *= vec3(0.25f, 1.0f, 0.25f); + break; + case 2: + color.rgb *= vec3(0.25f, 0.25f, 1.0f); + break; + case 3: + color.rgb *= vec3(1.0f, 1.0f, 0.25f); + break; + } + } +} + + diff --git a/StarEditor/Resources/Shaders/ImGui.hlsl b/StarEditor/Resources/Shaders/ImGui.hlsl new file mode 100644 index 00000000..85923885 --- /dev/null +++ b/StarEditor/Resources/Shaders/ImGui.hlsl @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2014-2024, NVIDIA CORPORATION. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +*/ + +#pragma stage : vertex + +struct Constants +{ + float2 invDisplaySize; +}; + +[[vk::push_constants]] ConstantBuffer g_Const : register(b0, space0); + +struct VS_INPUT +{ + float2 pos : POSITION; + float2 uv : TEXCOORD0; + float4 col : COLOR0; +}; + +struct PS_INPUT +{ + float4 out_pos : SV_POSITION; + float4 out_col : COLOR0; + float2 out_uv : TEXCOORD0; +}; + +PS_INPUT main(VS_INPUT input) +{ + PS_INPUT output; + output.out_pos.xy = input.pos.xy * g_Const.invDisplaySize * float2(2.0, -2.0) + float2(-1.0, 1.0); + output.out_pos.zw = float2(0, 1); + output.out_col = input.col; + output.out_uv = input.uv; + return output; +} + +#pragma stage : vertex + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float4 col : COLOR0; + float2 uv : TEXCOORD0; +}; + +sampler sampler0 : register(s0); +Texture2D texture0 : register(t0); + +float4 main(PS_INPUT input) : SV_Target +{ + float4 out_col = input.col * texture0.Sample(sampler0, input.uv); + return out_col; +} + + diff --git a/StarEditor/Resources/Shaders/Include/Common/Common.slh b/StarEditor/Resources/Shaders/Include/Common/Common.slh new file mode 100644 index 00000000..fb6eb613 --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/Common/Common.slh @@ -0,0 +1,3 @@ +#pragma once + +#define BIT(x) (1 << x) diff --git a/StarEditor/Resources/Shaders/Include/Common/GTAO.slh b/StarEditor/Resources/Shaders/Include/Common/GTAO.slh new file mode 100644 index 00000000..7a40d21a --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/Common/GTAO.slh @@ -0,0 +1,10 @@ +#pragma once +#include "Common.slh" + +#define HZ_AO_METHOD_GTAO BIT(1) +#define HZ_AO_METHOD_HBAO BIT(2) + +#define HZ_REFLECTION_OCCLUSION_METHOD_GTAO BIT(1) +#define HZ_REFLECTION_OCCLUSION_METHOD_HBAO BIT(2) + +#define XE_GTAO_OCCLUSION_TERM_SCALE (1.5f) // for packing in UNORM (because raw, pre-denoised occlusion term can overshoot 1 but will later average out to 1) diff --git a/StarEditor/Resources/Shaders/Include/GLSL/Buffers.glslh b/StarEditor/Resources/Shaders/Include/GLSL/Buffers.glslh new file mode 100644 index 00000000..c26b0ef9 --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/GLSL/Buffers.glslh @@ -0,0 +1,145 @@ +#pragma once + +// +// SET 1 +// + +layout (std140, set = 1, binding = 4) uniform ShadowData +{ + mat4 DirLightMatrices[4]; +} u_DirShadow; + +struct DirectionalLight +{ + vec3 Direction; + float ShadowAmount; + vec3 Radiance; + float Multiplier; +}; + +layout(std140, set = 1, binding = 5) uniform SceneData +{ + DirectionalLight DirectionalLights; + vec3 CameraPosition; // Offset = 32 + float EnvironmentMapIntensity; +} u_Scene; + + +struct PointLight +{ + vec3 Position; + float Multiplier; + vec3 Radiance; + float MinRadius; + float Radius; + float Falloff; + float LightSize; + bool CastsShadows; +}; + +layout(std140, set = 1, binding = 6) uniform PointLightData +{ + uint LightCount; + PointLight Lights[1000]; +} u_PointLights; + +struct SpotLight +{ + vec3 Position; + float Multiplier; + vec3 Direction; + float AngleAttenuation; + vec3 Radiance; + float Range; + float Angle; + float Falloff; + bool SoftShadows; + bool CastsShadows; +}; + +layout(std140, set = 1, binding = 7) uniform SpotLightData +{ + uint LightCount; + SpotLight Lights[1024]; +} u_SpotLights; + +layout(std140, set = 1, binding = 8) uniform SpotShadowData +{ + mat4 Mats[1024]; +} u_SpotLightMatrices; + +layout(std430, set = 1, binding = 9) buffer VisiblePointLightIndicesBuffer +{ + int Indices[]; +} s_VisiblePointLightIndicesBuffer; + +layout(std430, set = 1, binding = 10) writeonly buffer VisibleSpotLightIndicesBuffer +{ + int Indices[]; +} s_VisibleSpotLightIndicesBuffer; + +// +// SET 2 +// + +layout(std140, set = 2, binding = 0) uniform Camera +{ + mat4 ViewProjectionMatrix; + mat4 InverseViewProjectionMatrix; + mat4 ProjectionMatrix; + mat4 InverseProjectionMatrix; + mat4 ViewMatrix; + mat4 InverseViewMatrix; + vec2 NDCToViewMul; + vec2 NDCToViewAdd; + vec2 DepthUnpackConsts; + vec2 CameraTanHalfFOV; +} u_Camera; + +layout(std140, set = 2, binding = 1) uniform RendererData +{ + uniform vec4 CascadeSplits; + uniform int TilesCountX; + uniform bool ShowCascades; + uniform bool SoftShadows; + uniform float LightSize; + uniform float MaxShadowDistance; + uniform float ShadowFade; + uniform bool CascadeFading; + uniform float CascadeTransitionFade; + uniform bool ShowLightComplexity; +} u_RendererData; + +layout(std140, set = 2, binding = 2) uniform ScreenData +{ + vec2 InvFullResolution; + vec2 FullResolution; + vec2 InvHalfResolution; + vec2 HalfResolution; +} u_ScreenData; + + +layout(std140, set = 2, binding = 3) uniform HBAOData +{ + vec4 PerspectiveInfo; + vec2 InvQuarterResolution; + float RadiusToScreen; + float NegInvR2; + float NDotVBias; + float AOMultiplier; + float PowExponent; + bool IsOrtho; + vec4 Float2Offsets[16]; + vec4 Jitters[16]; + vec3 Padding; + float ShadowTolerance; +} u_HBAO; + + +const int AVG_NUM_BONES = 50; +const int MAX_ANIMATED_MESHES = 1024; + +layout (std140, set = 2, binding = 4) readonly buffer BoneTransforms +{ + mat4 BoneTransforms[MAX_ANIMATED_MESHES * AVG_NUM_BONES]; +} r_BoneTransforms; diff --git a/StarEditor/Resources/Shaders/Include/GLSL/Common.glslh b/StarEditor/Resources/Shaders/Include/GLSL/Common.glslh new file mode 100644 index 00000000..513cf5e4 --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/GLSL/Common.glslh @@ -0,0 +1,30 @@ +#pragma once + +const float PI = 3.141592; +const float TwoPI = 2 * PI; +const float Epsilon = 0.00001; + +vec3 RotateVectorAboutY(float angle, vec3 vec) +{ + angle = radians(angle); + mat3x3 rotationMatrix = { vec3(cos(angle),0.0,sin(angle)), + vec3(0.0,1.0,0.0), + vec3(-sin(angle),0.0,cos(angle)) }; + return rotationMatrix * vec; +} + +float Convert_sRGB_FromLinear(float theLinearValue) +{ + return theLinearValue <= 0.0031308f + ? theLinearValue * 12.92f + : pow(theLinearValue, 1.0f / 2.4f) * 1.055f - 0.055f; +} + +vec4 ToLinear(vec4 sRGB) +{ + bvec4 cutoff = lessThan(sRGB, vec4(0.04045)); + vec4 higher = pow((sRGB + vec4(0.055))/vec4(1.055), vec4(2.4)); + vec4 lower = sRGB/vec4(12.92); + + return mix(higher, lower, cutoff); +} diff --git a/StarEditor/Resources/Shaders/Include/GLSL/EnvironmentMapping.glslh b/StarEditor/Resources/Shaders/Include/GLSL/EnvironmentMapping.glslh new file mode 100644 index 00000000..003b51fe --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/GLSL/EnvironmentMapping.glslh @@ -0,0 +1,122 @@ +#pragma once +#include +// --------------------------------------------------------------------------------------------------- +// The following code (from Unreal Engine 4's paper) shows how to filter the environment map +// for different roughnesses. This is mean to be computed offline and stored in cube map mips, +// so turning this on online will cause poor performance +// Compute Van der Corput radical inverse +// See: http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html +float RadicalInverse_VdC(uint bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; // / 0x100000000 +} + +// Sample i-th point from Hammersley point set of NumSamples points total. +vec2 SampleHammersley(uint i, uint samples) +{ + float invSamples = 1.0 / float(samples); + return vec2(i * invSamples, RadicalInverse_VdC(i)); +} + +vec3 ImportanceSampleGGX(vec2 Xi, float Roughness, vec3 N) +{ + float a = Roughness * Roughness; + float Phi = 2 * PI * Xi.x; + float CosTheta = sqrt((1 - Xi.y) / (1 + (a * a - 1) * Xi.y)); + float SinTheta = sqrt(1 - CosTheta * CosTheta); + vec3 H; + H.x = SinTheta * cos(Phi); + H.y = SinTheta * sin(Phi); + H.z = CosTheta; + vec3 UpVector = abs(N.z) < 0.999 ? vec3(0, 0, 1) : vec3(1, 0, 0); + vec3 TangentX = normalize(cross(UpVector, N)); + vec3 TangentY = cross(N, TangentX); + // Tangent to world space + return TangentX * H.x + TangentY * H.y + N * H.z; +} + + +// Importance sample GGX normal distribution function for a fixed roughness value. +// This returns normalized half-vector between Li & Lo. +// For derivation see: http://blog.tobias-franke.eu/2014/03/30/notes_on_importance_sampling.html +vec3 SampleGGX(float u1, float u2, float roughness) +{ + float alpha = roughness * roughness; + + float cosTheta = sqrt((1.0 - u2) / (1.0 + (alpha*alpha - 1.0) * u2)); + float sinTheta = sqrt(1.0 - cosTheta*cosTheta); // Trig. identity + float phi = TwoPI * u1; + + // Convert to Cartesian upon return. + return vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); +} + +float TotalWeight = 0.0; + +vec3 PrefilterEnvMap(float Roughness, vec3 R) +{ + vec3 N = R; + vec3 V = R; + vec3 PrefilteredColor = vec3(0.0); + int NumSamples = 1024; + for (int i = 0; i < NumSamples; i++) + { + vec2 Xi = SampleHammersley(i, NumSamples); + vec3 H = ImportanceSampleGGX(Xi, Roughness, N); + vec3 L = 2 * dot(V, H) * H - V; + float NoL = clamp(dot(N, L), 0.0, 1.0); + if (NoL > 0) + { + //PrefilteredColor += texture(u_EnvRadianceTex, L).rgb * NoL; + TotalWeight += NoL; + } + } + return PrefilteredColor / TotalWeight; +} + +vec3 GetCubeMapTexCoord(vec2 imageSize) +{ + vec2 st = gl_GlobalInvocationID.xy / imageSize; + vec2 uv = 2.0 * vec2(st.x, 1.0 - st.y) - vec2(1.0); + + vec3 ret; + if (gl_GlobalInvocationID.z == 0) ret = vec3( 1.0, uv.y, -uv.x); + else if (gl_GlobalInvocationID.z == 1) ret = vec3( -1.0, uv.y, uv.x); + else if (gl_GlobalInvocationID.z == 2) ret = vec3( uv.x, 1.0, -uv.y); + else if (gl_GlobalInvocationID.z == 3) ret = vec3( uv.x, -1.0, uv.y); + else if (gl_GlobalInvocationID.z == 4) ret = vec3( uv.x, uv.y, 1.0); + else if (gl_GlobalInvocationID.z == 5) ret = vec3(-uv.x, uv.y, -1.0); + return normalize(ret); +} + +// Compute orthonormal basis for converting from tanget/shading space to world space. +void ComputeBasisVectors(const vec3 N, out vec3 S, out vec3 T) +{ + // Branchless select non-degenerate T. + T = cross(N, vec3(0.0, 1.0, 0.0)); + T = mix(cross(N, vec3(1.0, 0.0, 0.0)), T, step(Epsilon, dot(T, T))); + + T = normalize(T); + S = normalize(cross(N, T)); +} + +// Convert point from tangent/shading space to world space. +vec3 TangentToWorld(const vec3 v, const vec3 N, const vec3 S, const vec3 T) +{ + return S * v.x + T * v.y + N * v.z; +} + +// Uniformly sample point on a hemisphere. +// Cosine-weighted sampling would be a better fit for Lambertian BRDF but since this +// compute shader runs only once as a pre-processing step performance is not *that* important. +// See: "Physically Based Rendering" 2nd ed., section 13.6.1. +vec3 SampleHemisphere(float u1, float u2) +{ + const float u1p = sqrt(max(0.0, 1.0 - u1*u1)); + return vec3(cos(TwoPI*u2) * u1p, sin(TwoPI*u2) * u1p, u1); +} diff --git a/StarEditor/Resources/Shaders/Include/GLSL/Lighting.glslh b/StarEditor/Resources/Shaders/Include/GLSL/Lighting.glslh new file mode 100644 index 00000000..64248b3c --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/GLSL/Lighting.glslh @@ -0,0 +1,191 @@ +#pragma once + +#pragma stage : frag +#include +#include + +// Used in PBR shader +struct PBRParameters +{ + vec3 Albedo; + float Roughness; + float Metalness; + + vec3 Normal; + vec3 View; + float NdotV; +} m_Params; + +vec3 CalculateDirLights(vec3 F0) +{ + vec3 result = vec3(0.0); + for (int i = 0; i < 1; i++) //Only one light for now + { + if (u_Scene.DirectionalLights.Multiplier == 0.0) + continue; + + vec3 Li = u_Scene.DirectionalLights.Direction; + vec3 Lradiance = u_Scene.DirectionalLights.Radiance * u_Scene.DirectionalLights.Multiplier; + vec3 Lh = normalize(Li + m_Params.View); + + // Calculate angles between surface normal and various light vectors. + float cosLi = max(0.0, dot(m_Params.Normal, Li)); + float cosLh = max(0.0, dot(m_Params.Normal, Lh)); + + vec3 F = FresnelSchlickRoughness(F0, max(0.0, dot(Lh, m_Params.View)), m_Params.Roughness); + float D = NdfGGX(cosLh, m_Params.Roughness); + float G = GaSchlickGGX(cosLi, m_Params.NdotV, m_Params.Roughness); + + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseBRDF = kd * m_Params.Albedo; + + // Cook-Torrance + vec3 specularBRDF = (F * D * G) / max(Epsilon, 4.0 * cosLi * m_Params.NdotV); + specularBRDF = clamp(specularBRDF, vec3(0.0f), vec3(10.0f)); + result += (diffuseBRDF + specularBRDF) * Lradiance * cosLi; + } + return result; +} + +////////////////////////////////////////// +// POINT LIGHT +////////////////////////////////////////// + + +int GetPointLightBufferIndex(int i) +{ + ivec2 tileID = ivec2(gl_FragCoord) / ivec2(16, 16); + uint index = tileID.y * u_RendererData.TilesCountX + tileID.x; + + uint offset = index * 1024; + return s_VisiblePointLightIndicesBuffer.Indices[offset + i]; +} + +int GetPointLightCount() +{ + int result = 0; + for (int i = 0; i < u_PointLights.LightCount; i++) + { + uint lightIndex = GetPointLightBufferIndex(i); + if (lightIndex == -1) + break; + + result++; + } + + return result; +} + +vec3 CalculatePointLights(in vec3 F0, vec3 worldPos) +{ + vec3 result = vec3(0.0); + for (int i = 0; i < u_PointLights.LightCount; i++) + { + uint lightIndex = GetPointLightBufferIndex(i); + if (lightIndex == -1) + break; + + PointLight light = u_PointLights.Lights[lightIndex]; + vec3 Li = normalize(light.Position - worldPos); + float lightDistance = length(light.Position - worldPos); + vec3 Lh = normalize(Li + m_Params.View); + + float attenuation = clamp(1.0 - (lightDistance * lightDistance) / (light.Radius * light.Radius), 0.0, 1.0); + attenuation *= mix(attenuation, 1.0, light.Falloff); + + vec3 Lradiance = light.Radiance * light.Multiplier * attenuation; + + // Calculate angles between surface normal and various light vectors. + float cosLi = max(0.0, dot(m_Params.Normal, Li)); + float cosLh = max(0.0, dot(m_Params.Normal, Lh)); + + vec3 F = FresnelSchlickRoughness(F0, max(0.0, dot(Lh, m_Params.View)), m_Params.Roughness); + float D = NdfGGX(cosLh, m_Params.Roughness); + float G = GaSchlickGGX(cosLi, m_Params.NdotV, m_Params.Roughness); + + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseBRDF = kd * m_Params.Albedo; + + // Cook-Torrance + vec3 specularBRDF = (F * D * G) / max(Epsilon, 4.0 * cosLi * m_Params.NdotV); + specularBRDF = clamp(specularBRDF, vec3(0.0f), vec3(10.0f)); + result += (diffuseBRDF + specularBRDF) * Lradiance * cosLi; + } + return result; +} + + +////////////////////////////////////////// +// SPOT LIGHT +////////////////////////////////////////// + +int GetSpotLightBufferIndex(int i) +{ + ivec2 tileID = ivec2(gl_FragCoord) / ivec2(16, 16); + uint index = tileID.y * u_RendererData.TilesCountX + tileID.x; + + uint offset = index * 1024; + return s_VisibleSpotLightIndicesBuffer.Indices[offset + i]; +} + + +int GetSpotLightCount() +{ + int result = 0; + for (int i = 0; i < u_SpotLights.LightCount; i++) + { + uint lightIndex = GetSpotLightBufferIndex(i); + if (lightIndex == -1) + break; + + result++; + } + + return result; +} + +vec3 CalculateSpotLights(in vec3 F0, vec3 worldPos) +{ + vec3 result = vec3(0.0); + for (int i = 0; i < u_SpotLights.LightCount; i++) + { + uint lightIndex = GetSpotLightBufferIndex(i); + if (lightIndex == -1) + break; + + SpotLight light = u_SpotLights.Lights[lightIndex]; + vec3 Li = normalize(light.Position - worldPos); + float lightDistance = length(light.Position - worldPos); + + float cutoff = cos(radians(light.Angle * 0.5f)); + float scos = max(dot(Li, light.Direction), cutoff); + float rim = (1.0 - scos) / (1.0 - cutoff); + + float attenuation = clamp(1.0 - (lightDistance * lightDistance) / (light.Range * light.Range), 0.0, 1.0); + attenuation *= mix(attenuation, 1.0, light.Falloff); + attenuation *= 1.0 - pow(max(rim, 0.001), light.AngleAttenuation); + + vec3 Lradiance = light.Radiance * light.Multiplier * attenuation; + vec3 Lh = normalize(Li + m_Params.View); + + // Calculate angles between surface normal and various light vectors. + float cosLi = max(0.0, dot(m_Params.Normal, Li)); + float cosLh = max(0.0, dot(m_Params.Normal, Lh)); + + vec3 F = FresnelSchlickRoughness(F0, max(0.0, dot(Lh, m_Params.View)), m_Params.Roughness); + float D = NdfGGX(cosLh, m_Params.Roughness); + float G = GaSchlickGGX(cosLi, m_Params.NdotV, m_Params.Roughness); + + vec3 kd = (1.0 - F) * (1.0 - m_Params.Metalness); + vec3 diffuseBRDF = kd * m_Params.Albedo; + + // Cook-Torrance + vec3 specularBRDF = (F * D * G) / max(Epsilon, 4.0 * cosLi * m_Params.NdotV); + specularBRDF = clamp(specularBRDF, vec3(0.0f), vec3(10.0f)); + result += (diffuseBRDF + specularBRDF) * Lradiance * cosLi; + + } + return result; +} + + diff --git a/StarEditor/Resources/Shaders/Include/GLSL/PBR.glslh b/StarEditor/Resources/Shaders/Include/GLSL/PBR.glslh new file mode 100644 index 00000000..da770cb0 --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/GLSL/PBR.glslh @@ -0,0 +1,63 @@ +#pragma once +#include + +// GGX/Towbridge-Reitz normal distribution function. +// Uses Disney's reparametrization of alpha = roughness^2 +float NdfGGX(float cosLh, float roughness) +{ + float alpha = roughness * roughness; + float alphaSq = alpha * alpha; + + float denom = (cosLh * cosLh) * (alphaSq - 1.0) + 1.0; + return alphaSq / (PI * denom * denom); +} + +// Single term for separable Schlick-GGX below. +float GaSchlickG1(float cosTheta, float k) +{ + return cosTheta / (cosTheta * (1.0 - k) + k); +} + +// Schlick-GGX approximation of geometric attenuation function using Smith's method. +float GaSchlickGGX(float cosLi, float NdotV, float roughness) +{ + float r = roughness + 1.0; + float k = (r * r) / 8.0; // Epic suggests using this roughness remapping for analytic lights. + return GaSchlickG1(cosLi, k) * GaSchlickG1(NdotV, k); +} + +float GeometrySchlickGGX(float NdotV, float roughness) +{ + float r = (roughness + 1.0); + float k = (r * r) / 8.0; + + float nom = NdotV; + float denom = NdotV * (1.0 - k) + k; + + return nom / denom; +} + +float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) +{ + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float ggx2 = GeometrySchlickGGX(NdotV, roughness); + float ggx1 = GeometrySchlickGGX(NdotL, roughness); + + return ggx1 * ggx2; +} + +// Shlick's approximation of the Fresnel factor. +vec3 FresnelSchlick(vec3 F0, float cosTheta) +{ + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +vec3 FresnelSchlickRoughness(vec3 F0, float cosTheta, float roughness) +{ + return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); +} + + + + diff --git a/StarEditor/Resources/Shaders/Include/GLSL/PBR_Resources.glslh b/StarEditor/Resources/Shaders/Include/GLSL/PBR_Resources.glslh new file mode 100644 index 00000000..b07f633c --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/GLSL/PBR_Resources.glslh @@ -0,0 +1,19 @@ +#pragma once + +// PBR texture inputs +layout(set = 0, binding = 0) uniform sampler2D u_AlbedoTexture; +layout(set = 0, binding = 1) uniform sampler2D u_NormalTexture; +layout(set = 0, binding = 2) uniform sampler2D u_MetalnessTexture; +layout(set = 0, binding = 3) uniform sampler2D u_RoughnessTexture; + +// Environment maps +layout(set = 1, binding = 0) uniform samplerCube u_EnvRadianceTex; +layout(set = 1, binding = 1) uniform samplerCube u_EnvIrradianceTex; + +// TODO(Yan): move to header +// BRDF LUT +layout(set = 3, binding = 0) uniform sampler2D u_BRDFLUTTexture; + +// Shadow maps +layout(set = 1, binding = 2) uniform sampler2DArray u_ShadowMapTexture; +layout(set = 1, binding = 3) uniform sampler2D u_SpotShadowTexture; diff --git a/StarEditor/Resources/Shaders/Include/GLSL/ShadowMapping.glslh b/StarEditor/Resources/Shaders/Include/GLSL/ShadowMapping.glslh new file mode 100644 index 00000000..e176915a --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/GLSL/ShadowMapping.glslh @@ -0,0 +1,312 @@ + +#pragma once + +#if defined(__FRAGMENT_STAGE__) || defined(__COMPUTE_STAGE__) +#include +#include + +///////////////////////////////////////////// +// PCSS +///////////////////////////////////////////// + +float ShadowFade = 1.0; //TODO(Karim): A better placement of this? + +vec3 GetShadowMapCoords(vec3 shadowMapCoords[4], uint cascade) +{ + // NOTE(Yan): this exists because AMD doesn't seem to like indexing of varyings (causes artifacts) + switch (cascade) + { + case 0: return shadowMapCoords[0]; + case 1: return shadowMapCoords[1]; + case 2: return shadowMapCoords[2]; + case 3: return shadowMapCoords[3]; + } + return vec3(0.0f); +} + +float GetDirShadowBias() +{ + const float MINIMUM_SHADOW_BIAS = 0.002; + float bias = max(MINIMUM_SHADOW_BIAS * (1.0 - dot(m_Params.Normal, u_Scene.DirectionalLights.Direction)), MINIMUM_SHADOW_BIAS); + return bias; +} + +float HardShadows_DirectionalLight(sampler2DArray shadowMap, uint cascade, vec3 shadowCoords) +{ + float bias = GetDirShadowBias(); + float shadowMapDepth = texture(shadowMap, vec3(shadowCoords.xy * 0.5 + 0.5, cascade)).x; + return step(shadowCoords.z, shadowMapDepth + bias) * ShadowFade; +} + +// Penumbra +// this search area estimation comes from the following article: +// http://developer.download.nvidia.com/whitepapers/2008/PCSS_Integration.pdf +float SearchWidth(float uvLightSize, float receiverDistance) +{ + const float NEAR = 0.1; + return uvLightSize * (receiverDistance - NEAR) / u_Scene.CameraPosition.z; +} + +float SearchRegionRadiusUV(float zWorld) +{ + const float light_zNear = 0.0; // 0.01 gives artifacts? maybe because of ortho proj? + const float lightRadiusUV = 0.05; + return lightRadiusUV * (zWorld - light_zNear) / zWorld; +} + +const vec2 PoissonDistribution[64] = vec2[]( + vec2(-0.94201624, -0.39906216), + vec2(0.94558609, -0.76890725), + vec2(-0.094184101, -0.92938870), + vec2(0.34495938, 0.29387760), + vec2(-0.91588581, 0.45771432), + vec2(-0.81544232, -0.87912464), + vec2(-0.38277543, 0.27676845), + vec2(0.97484398, 0.75648379), + vec2(0.44323325, -0.97511554), + vec2(0.53742981, -0.47373420), + vec2(-0.26496911, -0.41893023), + vec2(0.79197514, 0.19090188), + vec2(-0.24188840, 0.99706507), + vec2(-0.81409955, 0.91437590), + vec2(0.19984126, 0.78641367), + vec2(0.14383161, -0.14100790), + vec2(-0.413923, -0.439757), + vec2(-0.979153, -0.201245), + vec2(-0.865579, -0.288695), + vec2(-0.243704, -0.186378), + vec2(-0.294920, -0.055748), + vec2(-0.604452, -0.544251), + vec2(-0.418056, -0.587679), + vec2(-0.549156, -0.415877), + vec2(-0.238080, -0.611761), + vec2(-0.267004, -0.459702), + vec2(-0.100006, -0.229116), + vec2(-0.101928, -0.380382), + vec2(-0.681467, -0.700773), + vec2(-0.763488, -0.543386), + vec2(-0.549030, -0.750749), + vec2(-0.809045, -0.408738), + vec2(-0.388134, -0.773448), + vec2(-0.429392, -0.894892), + vec2(-0.131597, 0.065058), + vec2(-0.275002, 0.102922), + vec2(-0.106117, -0.068327), + vec2(-0.294586, -0.891515), + vec2(-0.629418, 0.379387), + vec2(-0.407257, 0.339748), + vec2(0.071650, -0.384284), + vec2(0.022018, -0.263793), + vec2(0.003879, -0.136073), + vec2(-0.137533, -0.767844), + vec2(-0.050874, -0.906068), + vec2(0.114133, -0.070053), + vec2(0.163314, -0.217231), + vec2(-0.100262, -0.587992), + vec2(-0.004942, 0.125368), + vec2(0.035302, -0.619310), + vec2(0.195646, -0.459022), + vec2(0.303969, -0.346362), + vec2(-0.678118, 0.685099), + vec2(-0.628418, 0.507978), + vec2(-0.508473, 0.458753), + vec2(0.032134, -0.782030), + vec2(0.122595, 0.280353), + vec2(-0.043643, 0.312119), + vec2(0.132993, 0.085170), + vec2(-0.192106, 0.285848), + vec2(0.183621, -0.713242), + vec2(0.265220, -0.596716), + vec2(-0.009628, -0.483058), + vec2(-0.018516, 0.435703) + ); + +const vec2 poissonDisk[16] = vec2[]( + vec2(-0.94201624, -0.39906216), + vec2(0.94558609, -0.76890725), + vec2(-0.094184101, -0.92938870), + vec2(0.34495938, 0.29387760), + vec2(-0.91588581, 0.45771432), + vec2(-0.81544232, -0.87912464), + vec2(-0.38277543, 0.27676845), + vec2(0.97484398, 0.75648379), + vec2(0.44323325, -0.97511554), + vec2(0.53742981, -0.47373420), + vec2(-0.26496911, -0.41893023), + vec2(0.79197514, 0.19090188), + vec2(-0.24188840, 0.99706507), + vec2(-0.81409955, 0.91437590), + vec2(0.19984126, 0.78641367), + vec2(0.14383161, -0.14100790) + ); + +vec2 SamplePoisson(int index) +{ + return PoissonDistribution[index % 64]; +} + +///////////////////////////////////////////// +// Directional Shadows +///////////////////////////////////////////// + +float FindBlockerDistance_DirectionalLight(sampler2DArray shadowMap, uint cascade, vec3 shadowCoords, float uvLightSize) +{ + float bias = GetDirShadowBias(); + + int numBlockerSearchSamples = 64; + int blockers = 0; + float avgBlockerDistance = 0; + + float searchWidth = SearchRegionRadiusUV(shadowCoords.z); + for (int i = 0; i < numBlockerSearchSamples; i++) + { + float z = textureLod(shadowMap, vec3((shadowCoords.xy * 0.5 + 0.5) + SamplePoisson(i) * searchWidth, cascade), 0).r; + if (z < (shadowCoords.z - bias)) + { + blockers++; + avgBlockerDistance += z; + } + } + + if (blockers > 0) + return avgBlockerDistance / float(blockers); + + return -1; +} + +float PCF_DirectionalLight(sampler2DArray shadowMap, uint cascade, vec3 shadowCoords, float uvRadius) +{ + float bias = GetDirShadowBias(); + int numPCFSamples = 64; + + float sum = 0; + for (int i = 0; i < numPCFSamples; i++) + { + vec2 offset = SamplePoisson(i) * uvRadius; + float z = textureLod(shadowMap, vec3((shadowCoords.xy * 0.5 + 0.5) + offset, cascade), 0).r; + sum += step(shadowCoords.z - bias, z); + } + return sum / numPCFSamples; +} + +float NV_PCF_DirectionalLight(sampler2DArray shadowMap, uint cascade, vec3 shadowCoords, float uvRadius) +{ + float bias = GetDirShadowBias(); + + float sum = 0; + for (int i = 0; i < 16; i++) + { + vec2 offset = poissonDisk[i] * uvRadius; + float z = textureLod(shadowMap, vec3((shadowCoords.xy * 0.5 + 0.5) + offset, cascade), 0).r; + sum += step(shadowCoords.z - bias, z); + } + return sum / 16.0f; +} + +float PCSS_DirectionalLight(sampler2DArray shadowMap, uint cascade, vec3 shadowCoords, float uvLightSize) +{ + float blockerDistance = FindBlockerDistance_DirectionalLight(shadowMap, cascade, shadowCoords, uvLightSize); + if (blockerDistance == -1) // No occlusion + return 1.0f; + + float penumbraWidth = (shadowCoords.z - blockerDistance) / blockerDistance; + + float NEAR = 0.01; // Should this value be tweakable? + float uvRadius = penumbraWidth * uvLightSize * NEAR / shadowCoords.z; // Do we need to divide by shadowCoords.z? + uvRadius = min(uvRadius, 0.002f); + return PCF_DirectionalLight(shadowMap, cascade, shadowCoords, uvRadius) * ShadowFade; +} + +///////////////////////////////////////////// +// Spot light Shadows +///////////////////////////////////////////// + +const float SPOT_SHADOW_BIAS = 0.00025f; + +float HardShadows_SpotLight(sampler2D shadowMap, uint lightIndex, vec3 shadowCoords) +{ + float shadowMapDepth = texture(shadowMap, shadowCoords.xy).x; + return step(shadowCoords.z, shadowMapDepth + SPOT_SHADOW_BIAS); +} + +float FindBlockerDistance_SpotLight(sampler2D shadowMap, vec3 shadowCoords, float uvLightSize) +{ + int numBlockerSearchSamples = 16; + int blockers = 0; + float avgBlockerDistance = 0; + + float searchWidth = SearchRegionRadiusUV(shadowCoords.z); + searchWidth = 0.01; + for (int i = 0; i < numBlockerSearchSamples; i++) + { + float z = textureLod(shadowMap, vec2(shadowCoords.xy + SamplePoisson(i) * searchWidth), 0).r; + if (z < (shadowCoords.z - SPOT_SHADOW_BIAS)) + { + blockers++; + avgBlockerDistance += z; + } + } + + if (blockers > 0) + return avgBlockerDistance / float(blockers); + + return -1; +} + + +float PCF_SpotLight(sampler2D shadowMap, vec3 shadowCoords, float uvRadius) +{ + int numPCFSamples = 64; + + float sum = 0; + for (int i = 0; i < numPCFSamples; i++) + { + vec2 offset = SamplePoisson(i) * uvRadius; + float z = textureLod(shadowMap, (shadowCoords.xy) + offset, 0).r; + sum += step(shadowCoords.z - SPOT_SHADOW_BIAS, z); + } + return sum / float(numPCFSamples); +} + + +float PCSS_SpotLight(sampler2D shadowMap, uint lightIndex, vec3 shadowCoords, float uvLightSize) +{ + float blockerDistance = FindBlockerDistance_SpotLight(shadowMap, shadowCoords, uvLightSize); + if (blockerDistance == -1) // No occlusion + return 1.0f; + + float penumbraWidth = (shadowCoords.z - blockerDistance) / blockerDistance; + + float NEAR = 1.0; // Should this value be tweakable? + float uvRadius = penumbraWidth * uvLightSize * NEAR / shadowCoords.z; // Do we need to divide by shadowCoords.z? + uvRadius = min(uvRadius, 0.002f); + return PCF_SpotLight(shadowMap, shadowCoords, uvRadius); +} + +float SpotShadowCalculation(sampler2D SpotAtlas, vec3 worldPos) +{ + float shadow = 1.0; + for (int i = 0; i < u_SpotLights.LightCount; ++i) + { + int lightIndex = GetSpotLightBufferIndex(i); + if (lightIndex == -1) + break; + + SpotLight light = u_SpotLights.Lights[lightIndex]; + if(!light.CastsShadows) + continue; + + // NOTE(Yan): Mats[0] because we only support ONE shadow casting spot light at the moment and it MUST be index 0 + vec4 coords = u_SpotLightMatrices.Mats[0] * vec4(worldPos, 1.0f); + vec3 shadowMapCoords = (coords.xyz / coords.w); + shadowMapCoords.xy = shadowMapCoords.xy * 0.5 + 0.5; + if (any(lessThan(shadowMapCoords.xyz, vec3(0.0f))) || any(greaterThan(shadowMapCoords.xyz, vec3(1.0f)))) + continue; + + shadow *= light.SoftShadows ? PCSS_SpotLight(SpotAtlas, lightIndex, shadowMapCoords, light.Falloff) : HardShadows_SpotLight(SpotAtlas, lightIndex, shadowMapCoords); + } + + return shadow; +} + +#endif // defined(__FRAGMENT_STAGE) || defined(__COMPUTE_STAGE) diff --git a/StarEditor/Resources/Shaders/Include/HLSL/Buffers.hlslh b/StarEditor/Resources/Shaders/Include/HLSL/Buffers.hlslh new file mode 100644 index 00000000..b3662e02 --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/HLSL/Buffers.hlslh @@ -0,0 +1,26 @@ +#pragma once + +struct Camera +{ + float4x4 ViewProjectionMatrix; + float4x4 InverseViewProjectionMatrix; + float4x4 ProjectionMatrix; + float4x4 InverseProjectionMatrix; + float4x4 ViewMatrix; + float4x4 InverseViewMatrix; + float2 NDCToViewMul; + float2 NDCToViewAdd; + float2 DepthUnpackConsts; + float2 CameraTanHalfFOV; +}; +[[vk::binding(0,2)]] ConstantBuffer u_Camera; + +struct ScreenData +{ + float2 InvFullResolution; + float2 FullResolution; + float2 InvHalfResolution; + float2 HalfResolution; +}; +[[vk::binding(2,2)]] ConstantBuffer u_ScreenData; + \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/Include/HLSL/Types.hlslh b/StarEditor/Resources/Shaders/Include/HLSL/Types.hlslh new file mode 100644 index 00000000..61c52685 --- /dev/null +++ b/StarEditor/Resources/Shaders/Include/HLSL/Types.hlslh @@ -0,0 +1,23 @@ +#pragma once + +#if (USE_HALF_FLOAT_PRECISION != 0) + #if 1 // old fp16 approach ( 0.5f ? 1.0f : 0.0f; + outColor = result; +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/JumpFlood_Pass.glsl b/StarEditor/Resources/Shaders/JumpFlood_Pass.glsl new file mode 100644 index 00000000..d874e620 --- /dev/null +++ b/StarEditor/Resources/Shaders/JumpFlood_Pass.glsl @@ -0,0 +1,95 @@ +#version 450 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoords; + +struct VertexOutput +{ + vec2 TexCoords; + vec2 TexelSize; + vec2 UV[9]; +}; +layout (location = 0) out VertexOutput Output; + +layout(push_constant) uniform Uniforms +{ + vec2 TexelSize; + int Step; +} u_Renderer; + +void main() +{ + Output.TexCoords = a_TexCoords; + Output.TexelSize = u_Renderer.TexelSize; + + vec2 dx = vec2(u_Renderer.TexelSize.x, 0.0f) * u_Renderer.Step; + vec2 dy = vec2(0.0f, u_Renderer.TexelSize.y) * u_Renderer.Step; + + Output.UV[0] = Output.TexCoords; + + //Sample all pixels within a 3x3 block + Output.UV[1] = Output.TexCoords + dx; + Output.UV[2] = Output.TexCoords - dx; + Output.UV[3] = Output.TexCoords + dy; + Output.UV[4] = Output.TexCoords - dy; + Output.UV[5] = Output.TexCoords + dx + dy; + Output.UV[6] = Output.TexCoords + dx - dy; + Output.UV[7] = Output.TexCoords - dx + dy; + Output.UV[8] = Output.TexCoords - dx - dy; + + gl_Position = vec4(a_Position, 1.0f); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 o_Color; + +struct VertexOutput +{ + vec2 TexCoords; + vec2 TexelSize; + vec2 UV[9]; +}; +layout (location = 0) in VertexOutput Input; + +layout(set = 2, binding = 0) uniform sampler2D u_Texture; + +float ScreenDistance(vec2 v) +{ + float ratio = Input.TexelSize.x / Input.TexelSize.y; + v.x /= ratio; + return dot(v, v); +} + +void BoundsCheck(inout vec2 xy, vec2 uv) +{ + if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) + xy = vec2(1000.0f); +} + +void main() +{ + vec4 pixel = texture(u_Texture, Input.UV[0]); + + for (int j = 1; j <= 8; j++) + { + // Sample neighbouring pixel and make sure it's + // on the same side as us + vec4 n = texture(u_Texture, Input.UV[j]); + if (n.w != pixel.w) + n.xyz = vec3(0.0f); + + n.xy += Input.UV[j] - Input.UV[0]; + + // Invalidate out of bounds neighbours + BoundsCheck(n.xy, Input.UV[j]); + + float dist = ScreenDistance(n.xy); + if (dist < pixel.z) + pixel.xyz = vec3(n.xy, dist); + } + + o_Color = pixel; +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/LightCulling.glsl b/StarEditor/Resources/Shaders/LightCulling.glsl new file mode 100644 index 00000000..81c158c6 --- /dev/null +++ b/StarEditor/Resources/Shaders/LightCulling.glsl @@ -0,0 +1,212 @@ +// --------------------------------------- +// -- Hazel Engine light culling shader -- +// --------------------------------------- +// +// References: +// - SIGGRAPH 2011 - Rendering in Battlefield 3 +// - Implementation mostly adapted from https://github.com/bcrusco/Forward-Plus-Renderer +// +#version 450 core +#pragma stage : comp +#include + +layout(set = 1, binding = 0) uniform sampler2D u_DepthMap; + +#define TILE_SIZE 16 +#define MAX_LIGHT_COUNT 1024 + +struct Sphere +{ + vec3 c; // Center point. + float r; // Radius. +}; + +bool TestFrustumSides(vec3 c, float r, vec3 plane0, vec3 plane1, vec3 plane2, vec3 plane3) +{ + bool intersectingOrInside0 = dot(c, plane0) < r; + bool intersectingOrInside1 = dot(c, plane1) < r; + bool intersectingOrInside2 = dot(c, plane2) < r; + bool intersectingOrInside3 = dot(c, plane3) < r; + + return (intersectingOrInside0 && intersectingOrInside1 && + intersectingOrInside2 && intersectingOrInside3); +} + +// From XeGTAO +float ScreenSpaceToViewSpaceDepth(const float screenDepth) +{ + float depthLinearizeMul = u_Camera.DepthUnpackConsts.x; + float depthLinearizeAdd = u_Camera.DepthUnpackConsts.y; + // Optimised version of "-cameraClipNear / (cameraClipFar - projDepth * (cameraClipFar - cameraClipNear)) * cameraClipFar" + return depthLinearizeMul / (depthLinearizeAdd - screenDepth); +} +// Shared values between all the threads in the group +shared uint minDepthInt; +shared uint maxDepthInt; +shared uint visiblePointLightCount; +shared uint visibleSpotLightCount; +shared vec4 frustumPlanes[6]; + +// Shared local storage for visible indices, will be written out to the global buffer at the end +shared int visiblePointLightIndices[MAX_LIGHT_COUNT]; +shared int visibleSpotLightIndices[MAX_LIGHT_COUNT]; + +layout(local_size_x = TILE_SIZE, local_size_y = TILE_SIZE, local_size_z = 1) in; +void main() +{ + ivec2 location = ivec2(gl_GlobalInvocationID.xy); + ivec2 itemID = ivec2(gl_LocalInvocationID.xy); + ivec2 tileID = ivec2(gl_WorkGroupID.xy); + ivec2 tileNumber = ivec2(gl_NumWorkGroups.xy); + uint index = tileID.y * tileNumber.x + tileID.x; + + // Initialize shared global values for depth and light count + if (gl_LocalInvocationIndex == 0) + { + minDepthInt = 0xFFFFFFFF; + maxDepthInt = 0; + visiblePointLightCount = 0; + visibleSpotLightCount = 0; + } + + barrier(); + + // Step 1: Calculate the minimum and maximum depth values (from the depth buffer) for this group's tile + vec2 tc = vec2(location) / u_ScreenData.FullResolution; + float linearDepth = ScreenSpaceToViewSpaceDepth(textureLod(u_DepthMap, tc, 0).r); + + // Convert depth to uint so we can do atomic min and max comparisons between the threads + uint depthInt = floatBitsToUint(linearDepth); + atomicMin(minDepthInt, depthInt); + atomicMax(maxDepthInt, depthInt); + + barrier(); + + // Step 2: One thread should calculate the frustum planes to be used for this tile + if (gl_LocalInvocationIndex == 0) + { + // Convert the min and max across the entire tile back to float + float minDepth = uintBitsToFloat(minDepthInt); + float maxDepth = uintBitsToFloat(maxDepthInt); + + // Steps based on tile sale + vec2 negativeStep = (2.0 * vec2(tileID)) / vec2(tileNumber); + vec2 positiveStep = (2.0 * vec2(tileID + ivec2(1, 1))) / vec2(tileNumber); + + // Set up starting values for planes using steps and min and max z values + frustumPlanes[0] = vec4(1.0, 0.0, 0.0, 1.0 - negativeStep.x); // Left + frustumPlanes[1] = vec4(-1.0, 0.0, 0.0, -1.0 + positiveStep.x); // Right + frustumPlanes[2] = vec4(0.0, 1.0, 0.0, 1.0 - negativeStep.y); // Bottom + frustumPlanes[3] = vec4(0.0, -1.0, 0.0, -1.0 + positiveStep.y); // Top + frustumPlanes[4] = vec4(0.0, 0.0, -1.0, -minDepth); // Near + frustumPlanes[5] = vec4(0.0, 0.0, 1.0, maxDepth); // Far + + // Transform the first four planes + for (uint i = 0; i < 4; i++) + { + frustumPlanes[i] *= u_Camera.ViewProjectionMatrix; + frustumPlanes[i] /= length(frustumPlanes[i].xyz); + } + + // Transform the depth planes + frustumPlanes[4] *= u_Camera.ViewMatrix; + frustumPlanes[4] /= length(frustumPlanes[4].xyz); + frustumPlanes[5] *= u_Camera.ViewMatrix; + frustumPlanes[5] /= length(frustumPlanes[5].xyz); + } + + barrier(); + + // Step 3: Cull lights. + // Parallelize the threads against the lights now. + // Can handle 256 simultaniously. Anymore lights than that and additional passes are performed + const uint threadCount = TILE_SIZE * TILE_SIZE; + uint passCount = (u_PointLights.LightCount + threadCount - 1) / threadCount; + for (uint i = 0; i < passCount; i++) + { + // Get the lightIndex to test for this thread / pass. If the index is >= light count, then this thread can stop testing lights + uint lightIndex = i * threadCount + gl_LocalInvocationIndex; + if (lightIndex >= u_PointLights.LightCount) + break; + + vec4 position = vec4(u_PointLights.Lights[lightIndex].Position, 1.0f); + float radius = u_PointLights.Lights[lightIndex].Radius; + radius += radius * 0.3f; + + // Check if light radius is in frustum + float distance = 0.0; + for (uint j = 0; j < 6; j++) + { + distance = dot(position, frustumPlanes[j]) + radius; + if (distance <= 0.0) // No intersection + break; + } + + // If greater than zero, then it is a visible light + if (distance > 0.0) + { + // Add index to the shared array of visible indices + uint offset = atomicAdd(visiblePointLightCount, 1); + visiblePointLightIndices[offset] = int(lightIndex); + } + } + + passCount = (u_SpotLights.LightCount + threadCount - 1) / threadCount; + for (uint i = 0; i < passCount; i++) + { + // Get the lightIndex to test for this thread / pass. If the index is >= light count, then this thread can stop testing lights + uint lightIndex = i * threadCount + gl_LocalInvocationIndex; + if (lightIndex >= u_SpotLights.LightCount) + break; + + SpotLight light = u_SpotLights.Lights[lightIndex]; + float radius = light.Range; + // Check if light radius is in frustum + float distance = 0.0; + for (uint j = 0; j < 6; j++) + { + distance = dot(vec4(light.Position - light.Direction * (light.Range * 0.7), 1.0), frustumPlanes[j]) + radius * 1.3; + if (distance < 0.0) // No intersection + break; + } + + // If greater than zero, then it is a visible light + if (distance > 0.0) + { + // Add index to the shared array of visible indices + uint offset = atomicAdd(visibleSpotLightCount, 1); + visibleSpotLightIndices[offset] = int(lightIndex); + } + + } + + barrier(); + + // One thread should fill the global light buffer + if (gl_LocalInvocationIndex == 0) + { + const uint offset = index * MAX_LIGHT_COUNT; // Determine position in global buffer + for (uint i = 0; i < visiblePointLightCount; i++) + { + s_VisiblePointLightIndicesBuffer.Indices[offset + i] = visiblePointLightIndices[i]; + } + + for (uint i = 0; i < visibleSpotLightCount; i++) { + s_VisibleSpotLightIndicesBuffer.Indices[offset + i] = visibleSpotLightIndices[i]; + } + + if (visiblePointLightCount != MAX_LIGHT_COUNT) + { + // Unless we have totally filled the entire array, mark it's end with -1 + // Final shader step will use this to determine where to stop (without having to pass the light count) + s_VisiblePointLightIndicesBuffer.Indices[offset + visiblePointLightCount] = -1; + } + + if (visibleSpotLightCount != MAX_LIGHT_COUNT) + { + // Unless we have totally filled the entire array, mark it's end with -1 + // Final shader step will use this to determine where to stop (without having to pass the light count) + s_VisibleSpotLightIndicesBuffer.Indices[offset + visibleSpotLightCount] = -1; + } + } +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/PostProcessing/AO-Composite.glsl b/StarEditor/Resources/Shaders/PostProcessing/AO-Composite.glsl new file mode 100644 index 00000000..b0cd7097 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/AO-Composite.glsl @@ -0,0 +1,51 @@ +#version 430 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +layout(location = 0) out vec2 o_TexCoord; +void main() +{ + o_TexCoord = a_TexCoord; + gl_Position = vec4(a_Position.xy, 0.0, 1.0); +} + +#version 450 core +#pragma stage : frag +#include + +#define ENABLED_GTAO (__HZ_AO_METHOD & HZ_AO_METHOD_GTAO) +#define ENABLED_HBAO (__HZ_AO_METHOD & HZ_AO_METHOD_HBAO) + +#if ENABLED_GTAO +layout(set = 1, binding = 0) uniform usampler2D u_GTAOTex; +#endif + +#if ENABLED_HBAO +layout(binding = 1) uniform sampler2D u_HBAOTex; +#endif + +layout(location = 0) out vec4 o_Occlusion; +layout(location = 0) in vec2 vs_TexCoord; + + + +void main() +{ + float occlusion = 1.0f; +#if ENABLED_GTAO + #if __HZ_GTAO_COMPUTE_BENT_NORMALS + float ao = (texture(u_GTAOTex, vs_TexCoord).x >> 24) / 255.f; + #else + float ao = texture(u_GTAOTex, vs_TexCoord).x / 255.f; + #endif + occlusion = min(ao * XE_GTAO_OCCLUSION_TERM_SCALE, 1.0f); +#endif +#if ENABLED_HBAO + occlusion *= texture(u_HBAOTex, vs_TexCoord).x; +#endif + + o_Occlusion = occlusion.xxxx; +} + diff --git a/StarEditor/Resources/Shaders/PostProcessing/Bloom.glsl b/StarEditor/Resources/Shaders/PostProcessing/Bloom.glsl new file mode 100644 index 00000000..c06fd4be --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/Bloom.glsl @@ -0,0 +1,148 @@ +#version 450 core +#pragma stage : comp +layout(binding = 0, rgba32f) restrict writeonly uniform image2D o_Image; + +const float Epsilon = 1.0e-4; + +layout(binding = 1) uniform sampler2D u_Texture; +layout(binding = 2) uniform sampler2D u_BloomTexture; + +layout(push_constant) uniform Uniforms +{ + vec4 Params; // (x) threshold, (y) threshold - knee, (z) knee * 2, (w) 0.25 / knee + float LOD; + int Mode; // See defines below +} u_Uniforms; + +#define MODE_PREFILTER 0 +#define MODE_DOWNSAMPLE 1 +#define MODE_UPSAMPLE_FIRST 2 +#define MODE_UPSAMPLE 3 + +vec3 DownsampleBox13(sampler2D tex, float lod, vec2 uv, vec2 texelSize) +{ + // Center + vec3 A = textureLod(tex, uv, lod).rgb; + + texelSize *= 0.5f; // Sample from center of texels + + // Inner box + vec3 B = textureLod(tex, uv + texelSize * vec2(-1.0f, -1.0f), lod).rgb; + vec3 C = textureLod(tex, uv + texelSize * vec2(-1.0f, 1.0f), lod).rgb; + vec3 D = textureLod(tex, uv + texelSize * vec2(1.0f, 1.0f), lod).rgb; + vec3 E = textureLod(tex, uv + texelSize * vec2(1.0f, -1.0f), lod).rgb; + + // Outer box + vec3 F = textureLod(tex, uv + texelSize * vec2(-2.0f, -2.0f), lod).rgb; + vec3 G = textureLod(tex, uv + texelSize * vec2(-2.0f, 0.0f), lod).rgb; + vec3 H = textureLod(tex, uv + texelSize * vec2(0.0f, 2.0f), lod).rgb; + vec3 I = textureLod(tex, uv + texelSize * vec2(2.0f, 2.0f), lod).rgb; + vec3 J = textureLod(tex, uv + texelSize * vec2(2.0f, 2.0f), lod).rgb; + vec3 K = textureLod(tex, uv + texelSize * vec2(2.0f, 0.0f), lod).rgb; + vec3 L = textureLod(tex, uv + texelSize * vec2(-2.0f, -2.0f), lod).rgb; + vec3 M = textureLod(tex, uv + texelSize * vec2(0.0f, -2.0f), lod).rgb; + + // Weights + vec3 result = vec3(0.0); + // Inner box + result += (B + C + D + E) * 0.5f; + // Bottom-left box + result += (F + G + A + M) * 0.125f; + // Top-left box + result += (G + H + I + A) * 0.125f; + // Top-right box + result += (A + I + J + K) * 0.125f; + // Bottom-right box + result += (M + A + K + L) * 0.125f; + + // 4 samples each + result *= 0.25f; + + return result; +} + +// Quadratic color thresholding +// curve = (threshold - knee, knee * 2, 0.25 / knee) +vec4 QuadraticThreshold(vec4 color, float threshold, vec3 curve) +{ + // Maximum pixel brightness + float brightness = max(max(color.r, color.g), color.b); + // Quadratic curve + float rq = clamp(brightness - curve.x, 0.0, curve.y); + rq = (rq * rq) * curve.z; + color *= max(rq, brightness - threshold) / max(brightness, Epsilon); + return color; +} + +vec4 Prefilter(vec4 color, vec2 uv) +{ + float clampValue = 20.0f; + color = clamp(color, vec4(0.0f), vec4(clampValue)); + color = QuadraticThreshold(color, u_Uniforms.Params.x, u_Uniforms.Params.yzw); + return color; +} + +vec3 UpsampleTent9(sampler2D tex, float lod, vec2 uv, vec2 texelSize, float radius) +{ + vec4 offset = texelSize.xyxy * vec4(1.0f, 1.0f, -1.0f, 0.0f) * radius; + + // Center + vec3 result = textureLod(tex, uv, lod).rgb * 4.0f; + + result += textureLod(tex, uv - offset.xy, lod).rgb; + result += textureLod(tex, uv - offset.wy, lod).rgb * 2.0; + result += textureLod(tex, uv - offset.zy, lod).rgb; + + result += textureLod(tex, uv + offset.zw, lod).rgb * 2.0; + result += textureLod(tex, uv + offset.xw, lod).rgb * 2.0; + + result += textureLod(tex, uv + offset.zy, lod).rgb; + result += textureLod(tex, uv + offset.wy, lod).rgb * 2.0; + result += textureLod(tex, uv + offset.xy, lod).rgb; + + return result * (1.0f / 16.0f); +} + +layout(local_size_x = 4, local_size_y = 4) in; +void main() +{ + vec2 imgSize = vec2(imageSize(o_Image)); + + ivec2 invocID = ivec2(gl_GlobalInvocationID); + vec2 texCoords = vec2(float(invocID.x) / imgSize.x, float(invocID.y) / imgSize.y); + texCoords += (1.0f / imgSize) * 0.5f; + + vec2 texSize = vec2(textureSize(u_Texture, int(u_Uniforms.LOD))); + vec4 color = vec4(1, 0, 1, 1); + if (u_Uniforms.Mode == MODE_PREFILTER) + { + color.rgb = DownsampleBox13(u_Texture, 0, texCoords, 1.0f / texSize); + color = Prefilter(color, texCoords); + color.a = 1.0f; + } + else if (u_Uniforms.Mode == MODE_UPSAMPLE_FIRST) + { + vec2 bloomTexSize = vec2(textureSize(u_Texture, int(u_Uniforms.LOD + 1.0f))); + float sampleScale = 1.0f; + vec3 upsampledTexture = UpsampleTent9(u_Texture, u_Uniforms.LOD + 1.0f, texCoords, 1.0f / bloomTexSize, sampleScale); + + vec3 existing = textureLod(u_Texture, texCoords, u_Uniforms.LOD).rgb; + color.rgb = existing + upsampledTexture; + } + else if (u_Uniforms.Mode == MODE_UPSAMPLE) + { + vec2 bloomTexSize = vec2(textureSize(u_BloomTexture, int(u_Uniforms.LOD + 1.0f))); + float sampleScale = 1.0f; + vec3 upsampledTexture = UpsampleTent9(u_BloomTexture, u_Uniforms.LOD + 1.0f, texCoords, 1.0f / bloomTexSize, sampleScale); + + vec3 existing = textureLod(u_Texture, texCoords, u_Uniforms.LOD).rgb; + color.rgb = existing + upsampledTexture; + } + else if (u_Uniforms.Mode == MODE_DOWNSAMPLE) + { + // Downsample + color.rgb = DownsampleBox13(u_Texture, u_Uniforms.LOD, texCoords, 1.0f / texSize); + } + + imageStore(o_Image, ivec2(gl_GlobalInvocationID), color); +} diff --git a/StarEditor/Resources/Shaders/PostProcessing/DOF.glsl b/StarEditor/Resources/Shaders/PostProcessing/DOF.glsl new file mode 100644 index 00000000..d9f102c8 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/DOF.glsl @@ -0,0 +1,96 @@ +#version 450 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) out OutputBlock Output; + +void main() +{ + vec4 position = vec4(a_Position.xy, 0.0, 1.0); + Output.TexCoord = a_TexCoord; + gl_Position = position; +} + +#version 450 core +#pragma stage : frag + +#include + +layout(location = 0) out vec4 o_Color; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) in OutputBlock Input; + +layout (set = 1, binding = 5) uniform sampler2D u_Texture; +layout (set = 1, binding = 6) uniform sampler2D u_DepthTexture; + +layout(push_constant) uniform Uniforms +{ + vec2 DOFParams; // x = FocusDistance, y = BlurSize +} u_Uniforms; + +// WIP depth of field from https://blog.tuxedolabs.com/2018/05/04/bokeh-depth-of-field-in-single-pass.html +// NOTE(Yan): this is a pretty slow approach (especially on a full-size framebuffer) but it looks nice, +// so worth experimenting with (also like most things, would be better in compute) +const float GOLDEN_ANGLE = 2.39996323; +const float MAX_BLUR_SIZE = 20.0; +const float RAD_SCALE = 1.0; // Smaller = nicer blur, larger = faster + +float LinearizeDepth(const float screenDepth) +{ + float depthLinearizeMul = u_Camera.DepthUnpackConsts.x; + float depthLinearizeAdd = u_Camera.DepthUnpackConsts.y; + return depthLinearizeMul / (depthLinearizeAdd - screenDepth); +} + +float GetBlurSize(float depth, float focusPoint, float focusScale) +{ + float coc = clamp((1.0 / focusPoint - 1.0 / depth) * focusScale, -1.0, 1.0); + return abs(coc) * MAX_BLUR_SIZE; +} + +vec3 DepthOfField(vec2 texCoord, float focusPoint, float focusScale, vec2 texelSize) +{ + float centerDepth = LinearizeDepth(texture(u_DepthTexture, texCoord).r); + float centerSize = GetBlurSize(centerDepth, focusPoint, focusScale); + vec3 color = texture(u_Texture, texCoord).rgb; + float tot = 1.0; + float radius = RAD_SCALE; + for (float ang = 0.0; radius < MAX_BLUR_SIZE; ang += GOLDEN_ANGLE) + { + vec2 tc = texCoord + vec2(cos(ang), sin(ang)) * texelSize * radius; + vec3 sampleColor = texture(u_Texture, tc).rgb; + float sampleDepth = LinearizeDepth(texture(u_DepthTexture, tc).r); + float sampleSize = GetBlurSize(sampleDepth, focusPoint, focusScale); + if (sampleDepth > centerDepth) + sampleSize = clamp(sampleSize, 0.0, centerSize * 2.0); + float m = smoothstep(radius - 0.5, radius + 0.5, sampleSize); + color += mix(color / tot, sampleColor, m); + tot += 1.0; + radius += RAD_SCALE / radius; + } + return color /= tot; +} + +void main() +{ + ivec2 texSize = textureSize(u_Texture, 0); + vec2 fTexSize = vec2(float(texSize.x), float(texSize.y)); + + float focusPoint = u_Uniforms.DOFParams.x; + float blurScale = u_Uniforms.DOFParams.y; + + vec3 color = DepthOfField(Input.TexCoord, focusPoint, blurScale, 1.0 / fTexSize); + o_Color = vec4(color, 1.0); +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/PostProcessing/EdgeDetection.glsl b/StarEditor/Resources/Shaders/PostProcessing/EdgeDetection.glsl new file mode 100644 index 00000000..632e5660 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/EdgeDetection.glsl @@ -0,0 +1,83 @@ +#version 450 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) out OutputBlock Output; + +void main() +{ + vec4 position = vec4(a_Position.xy, 0.0, 1.0); + Output.TexCoord = a_TexCoord; + gl_Position = position; +} + +#version 450 core +#pragma stage : frag + +#include + +layout(location = 0) out vec4 o_Color; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) in OutputBlock Input; + +layout (set = 1, binding = 0) uniform sampler2D u_ViewNormalsTexture; +layout (set = 1, binding = 1) uniform sampler2D u_DepthTexture; + +float LinearizeDepth(const float screenDepth) +{ + float depthLinearizeMul = u_Camera.DepthUnpackConsts.x; + float depthLinearizeAdd = u_Camera.DepthUnpackConsts.y; + return depthLinearizeMul / (depthLinearizeAdd - screenDepth); +} + +float checkSame(vec4 center, float centerDepth, vec4 samplef, float sampleDepth, vec2 sensitivity) +{ + vec2 centerNormal = center.xy; + vec2 sampleNormal = samplef.xy; + + vec2 diffNormal = abs(centerNormal - sampleNormal) * sensitivity.x; + bool isSameNormal = (diffNormal.x + diffNormal.y) < 0.1; + float diffDepth = abs(centerDepth - sampleDepth) * sensitivity.y; + bool isSameDepth = diffDepth < 0.1; + + return (isSameNormal && isSameDepth) ? 1.0 : 0.0; + return (isSameNormal) ? 1.0 : 0.0; +} + +void main() +{ + ivec2 texSize = textureSize(u_ViewNormalsTexture, 0); + vec2 fTexSize = vec2(float(texSize.x), float(texSize.y)); + + vec2 sensitivity = (vec2(0.3, 1.5) * fTexSize.y / 800.0); + vec2 singleTexel = vec2(1.0) / fTexSize; + + vec4 sample1 = texture(u_ViewNormalsTexture, Input.TexCoord + singleTexel); + vec4 sample2 = texture(u_ViewNormalsTexture, Input.TexCoord + -singleTexel); + vec4 sample3 = texture(u_ViewNormalsTexture, Input.TexCoord + vec2(-singleTexel.x, singleTexel.y)); + vec4 sample4 = texture(u_ViewNormalsTexture, Input.TexCoord + vec2(singleTexel.x, -singleTexel.y)); + + float sampleDepth1 = LinearizeDepth(texture(u_DepthTexture, Input.TexCoord + singleTexel).r) / 10.0; + float sampleDepth2 = LinearizeDepth(texture(u_DepthTexture, Input.TexCoord + -singleTexel).r)/ 10.0; + float sampleDepth3 = LinearizeDepth(texture(u_DepthTexture, Input.TexCoord + vec2(-singleTexel.x, singleTexel.y)).r)/ 10.0; + float sampleDepth4 = LinearizeDepth(texture(u_DepthTexture, Input.TexCoord + vec2(singleTexel.x, -singleTexel.y)).r)/ 10.0; + + float edge = checkSame(sample1, sampleDepth1, sample2, sampleDepth2, sensitivity) * checkSame(sample3, sampleDepth3, sample4, sampleDepth4, sensitivity); + + //float rawDepth = texture(u_DepthTexture, Input.TexCoord).r; + //float depth = LinearizeDepth(rawDepth); + + o_Color = vec4(vec3(edge), 1.0); +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/PostProcessing/GTAO-Denoise.glsl b/StarEditor/Resources/Shaders/PostProcessing/GTAO-Denoise.glsl new file mode 100644 index 00000000..ed067330 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/GTAO-Denoise.glsl @@ -0,0 +1,189 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016-2021, Intel Corporation +// +// SPDX-License-Identifier: MIT +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#version 450 core +#pragma stage : comp +#include +#include + +layout(set = 1, binding = 0, r32ui) uniform writeonly uimage2D o_AOTerm; +layout(set = 1, binding = 1) uniform sampler2D u_Edges; +layout(set = 1, binding = 2) uniform usampler2D u_AOTerm; + +layout(push_constant) uniform DenoiseConstants +{ + float DenoiseBlurBeta; + bool HalfRes; +} u_Settings; + +#if __HZ_GTAO_COMPUTE_BENT_NORMALS +#define AOTermType vec4 // .xyz is bent normal, .w is visibility term +#else +#define AOTermType float // .x is visibility term +#endif + + +vec4 XeGTAO_R8G8B8A8_UNORM_to_FLOAT4(uint packedInput) +{ + vec4 unpackedOutput; + unpackedOutput.x = float(packedInput & 0x000000ff) / 255.f; + unpackedOutput.y = float(((packedInput >> 8) & 0x000000ff)) / 255.f; + unpackedOutput.z = float(((packedInput >> 16) & 0x000000ff)) / 255.f; + unpackedOutput.w = float(packedInput >> 24) / 255.f; + return unpackedOutput; +} + +void XeGTAO_DecodeVisibilityBentNormal(const uint packedValue, out float visibility, out vec3 bentNormal) +{ + vec4 decoded = XeGTAO_R8G8B8A8_UNORM_to_FLOAT4(packedValue); + bentNormal = decoded.xyz * 2.0.xxx - 1.0.xxx; // could normalize - don't want to since it's done so many times, better to do it at the final step only + visibility = decoded.w; +} + + +void XeGTAO_DecodeGatherPartial(const uvec4 packedValue, out AOTermType outDecoded[4]) +{ + for( int i = 0; i < 4; i++ ) + { + #if __HZ_GTAO_COMPUTE_BENT_NORMALS + XeGTAO_DecodeVisibilityBentNormal(packedValue[i], outDecoded[i].w, outDecoded[i].xyz); + #else + outDecoded[i] = float(packedValue[i]) / 255.0; + #endif + } +} + +vec4 XeGTAO_UnpackEdges(float _packedVal) +{ + uint packedVal = uint(_packedVal * 255.5); + vec4 edgesLRTB; + edgesLRTB.x = float((packedVal >> 6) & 0x03) / 3.0; // there's really no need for mask (as it's an 8 bit input) but I'll leave it in so it doesn't cause any trouble in the future + edgesLRTB.y = float((packedVal >> 4) & 0x03) / 3.0; + edgesLRTB.z = float((packedVal >> 2) & 0x03) / 3.0; + edgesLRTB.w = float((packedVal >> 0) & 0x03) / 3.0; + + return clamp(edgesLRTB, 0.0, 1.0); +} + +void XeGTAO_AddSample(AOTermType ssaoValue, float edgeValue, inout AOTermType sum, inout float sumWeight) +{ + float weight = edgeValue; + + sum += (weight * ssaoValue); + sumWeight += weight; +} + +uint XeGTAO_FLOAT4_to_R8G8B8A8_UNORM(vec4 unpackedInput) +{ + return ((uint(clamp(unpackedInput.x, 0.0, 1.0) * 255.f + 0.5f)) | + (uint(clamp(unpackedInput.y, 0.0, 1.0) * 255.f + 0.5f) << 8 ) | + (uint(clamp(unpackedInput.z, 0.0, 1.0) * 255.f + 0.5f) << 16 ) | + (uint(clamp(unpackedInput.w, 0.0, 1.0) * 255.f + 0.5f) << 24 ) ); +} + + +uint XeGTAO_EncodeVisibilityBentNormal(float visibility, vec3 bentNormal) +{ + return XeGTAO_FLOAT4_to_R8G8B8A8_UNORM(vec4(bentNormal * 0.5 + 0.5, visibility)); +} + +void XeGTAO_Output(ivec2 pixCoord, AOTermType outputValue) +{ +#if __HZ_GTAO_COMPUTE_BENT_NORMALS + float visibility = outputValue.w; + vec3 bentNormal = normalize(outputValue.xyz); + imageStore(o_AOTerm, pixCoord, uint(XeGTAO_EncodeVisibilityBentNormal(visibility, bentNormal)).xxxx); +#else + imageStore(o_AOTerm, pixCoord, uint(min(outputValue * 255.0 + 0.5, 255.f)).xxxx); +#endif +} + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; +void main() +{ + + const float blurAmount = (u_Settings.DenoiseBlurBeta / 5.0); + const float diagWeight = 0.85 * 0.5; + + AOTermType aoTerm[2]; // pixel pixCoordBase and pixel pixCoordBase + ivec2( 1, 0 ) + vec4 edgesC_LRTB[2]; + float weightTL[2]; + float weightTR[2]; + float weightBL[2]; + float weightBR[2]; + + // we're computing 2 horizontal pixels at a time (performance optimization) + ivec2 pixCoordBase = ivec2(gl_GlobalInvocationID.xy * ivec2(2, 1)); + + // gather edge and visibility quads, used later + vec2 gatherCenter = vec2(pixCoordBase) * u_ScreenData.InvFullResolution * (1 + int(u_Settings.HalfRes)); + + vec4 edgesQ0 = textureGather(u_Edges, gatherCenter); + vec4 edgesQ1 = textureGatherOffset(u_Edges, gatherCenter, ivec2(2, 0)); + vec4 edgesQ2 = textureGatherOffset(u_Edges, gatherCenter, ivec2(1, 2)); + + AOTermType visQ0[4]; XeGTAO_DecodeGatherPartial(textureGatherOffset(u_AOTerm, gatherCenter, ivec2(0, 0)), visQ0); + AOTermType visQ1[4]; XeGTAO_DecodeGatherPartial(textureGatherOffset(u_AOTerm, gatherCenter, ivec2(2, 0)), visQ1); + AOTermType visQ2[4]; XeGTAO_DecodeGatherPartial(textureGatherOffset(u_AOTerm, gatherCenter, ivec2(0, 2)), visQ2); + AOTermType visQ3[4]; XeGTAO_DecodeGatherPartial(textureGatherOffset(u_AOTerm, gatherCenter, ivec2(2, 2)), visQ3); + + for(int side = 0; side < 2; side++) + { + const ivec2 pixCoord = ivec2(pixCoordBase.x + side, pixCoordBase.y); + + vec4 edgesL_LRTB = XeGTAO_UnpackEdges((side == 0) ? (edgesQ0.x) : (edgesQ0.y)); + vec4 edgesT_LRTB = XeGTAO_UnpackEdges((side == 0) ? (edgesQ0.z) : (edgesQ1.w)); + vec4 edgesR_LRTB = XeGTAO_UnpackEdges((side == 0) ? (edgesQ1.x) : (edgesQ1.y)); + vec4 edgesB_LRTB = XeGTAO_UnpackEdges((side == 0) ? (edgesQ2.w) : (edgesQ2.z)); + + edgesC_LRTB[side] = XeGTAO_UnpackEdges((side == 0) ? edgesQ0.y : edgesQ1.x); + + // Edges aren't perfectly symmetrical: edge detection algorithm does not guarantee that a left edge on the right pixel will match the right edge on the left pixel (although + // they will match in majority of cases). This line further enforces the symmetricity, creating a slightly sharper blur. Works real nice with TAA. + edgesC_LRTB[side] *= vec4(edgesL_LRTB.y, edgesR_LRTB.x, edgesT_LRTB.w, edgesB_LRTB.z); + +#if 1 // this allows some small amount of AO leaking from neighbours if there are 3 or 4 edges; this reduces both spatial and temporal aliasing + const float leak_threshold = 2.5; const float leak_strength = 0.5; + float edginess = (clamp(4.0 - leak_threshold - dot(edgesC_LRTB[side], vec4(1.0)), 0.0, 1.0) / (4 - leak_threshold)) * leak_strength; + edgesC_LRTB[side] = clamp(edgesC_LRTB[side] + edginess, 0.0, 1.0); +#endif + + // for diagonals; used by first and second pass + weightTL[side] = diagWeight * (edgesC_LRTB[side].x * edgesL_LRTB.z + edgesC_LRTB[side].z * edgesT_LRTB.x); + weightTR[side] = diagWeight * (edgesC_LRTB[side].z * edgesT_LRTB.y + edgesC_LRTB[side].y * edgesR_LRTB.z); + weightBL[side] = diagWeight * (edgesC_LRTB[side].w * edgesB_LRTB.x + edgesC_LRTB[side].x * edgesL_LRTB.w); + weightBR[side] = diagWeight * (edgesC_LRTB[side].y * edgesR_LRTB.w + edgesC_LRTB[side].w * edgesB_LRTB.y); + + // first pass + AOTermType ssaoValue = side == 0 ? visQ0[1] : visQ1[0]; + AOTermType ssaoValueL = side == 0 ? visQ0[0] : visQ0[1]; + AOTermType ssaoValueT = side == 0 ? visQ0[2] : visQ1[3]; + AOTermType ssaoValueR = side == 0 ? visQ1[0] : visQ1[1]; + AOTermType ssaoValueB = side == 0 ? visQ2[2] : visQ3[3]; + AOTermType ssaoValueTL = side == 0 ? visQ0[3] : visQ0[2]; + AOTermType ssaoValueBR = side == 0 ? visQ3[3] : visQ3[2]; + AOTermType ssaoValueTR = side == 0 ? visQ1[3] : visQ1[2]; + AOTermType ssaoValueBL = side == 0 ? visQ2[3] : visQ2[2]; + + float sumWeight = blurAmount; + AOTermType sum = ssaoValue * sumWeight; + + XeGTAO_AddSample(ssaoValueL, edgesC_LRTB[side].x, sum, sumWeight); + XeGTAO_AddSample(ssaoValueR, edgesC_LRTB[side].y, sum, sumWeight); + XeGTAO_AddSample(ssaoValueT, edgesC_LRTB[side].z, sum, sumWeight); + XeGTAO_AddSample(ssaoValueB, edgesC_LRTB[side].w, sum, sumWeight); + + XeGTAO_AddSample(ssaoValueTL, weightTL[side], sum, sumWeight); + XeGTAO_AddSample(ssaoValueTR, weightTR[side], sum, sumWeight); + XeGTAO_AddSample(ssaoValueBL, weightBL[side], sum, sumWeight); + XeGTAO_AddSample(ssaoValueBR, weightBR[side], sum, sumWeight); + + aoTerm[side] = sum / sumWeight; + + XeGTAO_Output(pixCoord, aoTerm[side]); + } +} + + diff --git a/StarEditor/Resources/Shaders/PostProcessing/GTAO.hlsl b/StarEditor/Resources/Shaders/PostProcessing/GTAO.hlsl new file mode 100644 index 00000000..83dab4ed --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/GTAO.hlsl @@ -0,0 +1,474 @@ +#pragma stage : comp +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016-2021, Intel Corporation +// +// SPDX-License-Identifier: MIT +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// XeGTAO is based on GTAO/GTSO "Jimenez et al. / Practical Real-Time Strategies for Accurate Indirect Occlusion", +// https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf +// +// Implementation: Filip Strugar (filip.strugar@intel.com), Steve Mccalla (\_/) +// Details: https://github.com/GameTechDev/XeGTAO (")_(") +// +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef XE_GTAO_USE_HALF_FLOAT_PRECISION +#define XE_GTAO_USE_HALF_FLOAT_PRECISION 1 +#endif +#include +#include +#include + +// input output textures for the second pass +[[vk::binding(1, 1)]] Texture2D u_ViewNormal; // source normal map +[[vk::binding(2, 1)]] Texture2D u_HilbertLut; // hilbert lookup table +[[vk::combinedImageSampler]] [[vk::binding(3, 1)]] Texture2D u_HiZDepth; +[[vk::combinedImageSampler]] [[vk::binding(3, 1)]] SamplerState u_samplerPointClamp; +[[vk::binding(4, 1)]] RWTexture2D o_AOwBentNormals; // output AO term (includes bent normals if enabled - packed as R11G11B10 scaled by AO) +[[vk::binding(5, 1)]] RWTexture2D o_Edges; // output depth-based edges used by the denoiser + +#define XE_GTAO_DEPTH_MIP_LEVELS 5 // this one is hard-coded to 5 for now +#define XE_GTAO_NUMTHREADS_X 16 // these can be changed +#define XE_GTAO_NUMTHREADS_Y 16 // these can be changed + +struct GTAOConstants +{ + float2 NDCToViewMul_x_PixelSize; + float EffectRadius; // world (viewspace) maximum size of the shadow + float EffectFalloffRange; + + float RadiusMultiplier; + float FinalValuePower; + float DenoiseBlurBeta; + bool HalfRes; + + float SampleDistributionPower; + float ThinOccluderCompensation; + float DepthMIPSamplingOffset; + int NoiseIndex; // frameIndex % 64 if using TAA or 0 otherwise + + float2 HZBUVFactor; + float ShadowTolerance; + float Padding; +}; +[[vk::push_constant]] ConstantBuffer u_GTAOConsts; + + + +#ifndef XE_GTAO_USE_DEFAULT_CONSTANTS +#define XE_GTAO_USE_DEFAULT_CONSTANTS 0 +#endif + +// some constants reduce performance if provided as dynamic values; if these constants are not required to be dynamic and they match default values, +// set XE_GTAO_USE_DEFAULT_CONSTANTS and the code will compile into a more efficient shader +#define XE_GTAO_DEFAULT_RADIUS_MULTIPLIER (1.457f ) // allows us to use different value as compared to ground truth radius to counter inherent screen space biases +#define XE_GTAO_DEFAULT_FALLOFF_RANGE (0.615f ) // distant samples contribute less +#define XE_GTAO_DEFAULT_SAMPLE_DISTRIBUTION_POWER (2.0f ) // small crevices more important than big surfaces +#define XE_GTAO_DEFAULT_THIN_OCCLUDER_COMPENSATION (0.0f ) // the new 'thickness heuristic' approach +#define XE_GTAO_DEFAULT_FINAL_VALUE_POWER (2.2f ) // modifies the final ambient occlusion value using power function - this allows some of the above heuristics to do different things +#define XE_GTAO_DEFAULT_DEPTH_MIP_SAMPLING_OFFSET (3.30f ) // main trade-off between performance (memory bandwidth) and quality (temporal stability is the first affected, thin objects next) + +#define XE_GTAO_PI (3.1415926535897932384626433832795) +#define XE_GTAO_PI_HALF (1.5707963267948966192313216916398) + +#if defined(XE_GTAO_FP32_DEPTHS) && XE_GTAO_USE_HALF_FLOAT_PRECISION +#error Using XE_GTAO_USE_HALF_FLOAT_PRECISION with 32bit depths is not supported yet unfortunately (it is possible to apply fp16 on parts not related to depth but this has not been done yet) +#endif + +uint XeGTAO_FLOAT4_to_R8G8B8A8_UNORM(lpfloat4 unpackedInput) +{ + return ((uint(saturate(unpackedInput.x) * (lpfloat)255 + (lpfloat)0.5)) | + (uint(saturate(unpackedInput.y) * (lpfloat)255 + (lpfloat)0.5) << 8) | + (uint(saturate(unpackedInput.z) * (lpfloat)255 + (lpfloat)0.5) << 16) | + (uint(saturate(unpackedInput.w) * (lpfloat)255 + (lpfloat)0.5) << 24)); +} + + +// Inputs are screen XY and viewspace depth, output is viewspace position +float3 XeGTAO_ComputeViewspacePosition(const float2 screenPos, const float viewspaceDepth) +{ + float3 ret; + ret.xy = mad(u_Camera.NDCToViewMul, screenPos.xy, u_Camera.NDCToViewAdd) * viewspaceDepth; + ret.z = viewspaceDepth; + return ret; +} + +lpfloat4 XeGTAO_CalculateEdges(const lpfloat centerZ, const lpfloat leftZ, const lpfloat rightZ, const lpfloat topZ, const lpfloat bottomZ) +{ + lpfloat4 edgesLRTB = lpfloat4(leftZ, rightZ, topZ, bottomZ) - (lpfloat)centerZ; + + lpfloat slopeLR = (edgesLRTB.y - edgesLRTB.x) * 0.5; + lpfloat slopeTB = (edgesLRTB.w - edgesLRTB.z) * 0.5; + lpfloat4 edgesLRTBSlopeAdjusted = edgesLRTB + lpfloat4(slopeLR, -slopeLR, slopeTB, -slopeTB); + edgesLRTB = min(abs(edgesLRTB), abs(edgesLRTBSlopeAdjusted)); + return lpfloat4(saturate((1.25 - edgesLRTB / (centerZ * 0.011)))); +} + +// packing/unpacking for edges; 2 bits per edge mean 4 gradient values (0, 0.33, 0.66, 1) for smoother transitions! +lpfloat XeGTAO_PackEdges(lpfloat4 edgesLRTB) +{ + // integer version: + // edgesLRTB = saturate(edgesLRTB) * 2.9.xxxx + 0.5.xxxx; + // return (((uint)edgesLRTB.x) << 6) + (((uint)edgesLRTB.y) << 4) + (((uint)edgesLRTB.z) << 2) + (((uint)edgesLRTB.w)); + // + // optimized, should be same as above + edgesLRTB = round(saturate(edgesLRTB) * 2.9); + return dot(edgesLRTB, lpfloat4(64.0 / 255.0, 16.0 / 255.0, 4.0 / 255.0, 1.0 / 255.0)); +} + +// http://h14s.p5r.org/2012/09/0x5f3759df.html, [Drobot2014a] Low Level Optimizations for GCN, https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf slide 63 +lpfloat XeGTAO_FastSqrt(float x) +{ + return (lpfloat)(asfloat(0x1fbd1df5 + (asint(x) >> 1))); +} +// input [-1, 1] and output [0, PI], from https://seblagarde.wordpress.com/2014/12/01/inverse-trigonometric-functions-gpu-optimization-for-amd-gcn-architecture/ +lpfloat XeGTAO_FastACos(lpfloat inX) +{ + const lpfloat PI = 3.141593; + const lpfloat HALF_PI = 1.570796; + lpfloat x = abs(inX); + lpfloat res = -0.156583 * x + HALF_PI; + res *= XeGTAO_FastSqrt(1.0 - x); + return (inX >= 0) ? res : PI - res; +} + +uint XeGTAO_EncodeVisibilityBentNormal(lpfloat visibility, lpfloat3 bentNormal) +{ + return XeGTAO_FLOAT4_to_R8G8B8A8_UNORM(lpfloat4(bentNormal * 0.5 + 0.5, visibility)); +} + +void XeGTAO_OutputWorkingTerm(const uint2 pixCoord, lpfloat visibility, lpfloat3 bentNormal, RWTexture2D o_AOwBentNormals) +{ + visibility = (lpfloat)saturate(visibility / XE_GTAO_OCCLUSION_TERM_SCALE); +#if __HZ_GTAO_COMPUTE_BENT_NORMALS + o_AOwBentNormals[pixCoord] = XeGTAO_EncodeVisibilityBentNormal(visibility, bentNormal); +#else + o_AOwBentNormals[pixCoord] = uint(visibility * 255.0 + 0.5); +#endif +} + +// "Efficiently building a matrix to rotate one vector to another" +// http://cs.brown.edu/research/pubs/pdfs/1999/Moller-1999-EBA.pdf / https://dl.acm.org/doi/10.1080/10867651.1999.10487509 +// (using https://github.com/assimp/assimp/blob/master/include/assimp/matrix3x3.inl#L275 as a code reference as it seems to be best) +lpfloat3x3 XeGTAO_RotFromToMatrix(lpfloat3 from, lpfloat3 to) +{ + const lpfloat e = dot(from, to); + const lpfloat f = abs(e); //(e < 0)? -e:e; + + // WARNING: This has not been tested/worked through, especially not for 16bit floats; seems to work in our special use case (from is always {0, 0, -1}) but wouldn't use it in general + if (f > lpfloat(1.0 - 0.0003)) + return lpfloat3x3(1, 0, 0, 0, 1, 0, 0, 0, 1); + + const lpfloat3 v = cross(from, to); + /* ... use this hand optimized version (9 mults less) */ + const lpfloat h = (1.0) / (1.0 + e); /* optimization by Gottfried Chen */ + const lpfloat hvx = h * v.x; + const lpfloat hvz = h * v.z; + const lpfloat hvxy = hvx * v.y; + const lpfloat hvxz = hvx * v.z; + const lpfloat hvyz = hvz * v.y; + + lpfloat3x3 mtx; + mtx[0][0] = e + hvx * v.x; + mtx[0][1] = hvxy - v.z; + mtx[0][2] = hvxz + v.y; + + mtx[1][0] = hvxy + v.z; + mtx[1][1] = e + h * v.y * v.y; + mtx[1][2] = hvyz - v.x; + + mtx[2][0] = hvxz - v.y; + mtx[2][1] = hvyz + v.x; + mtx[2][2] = e + hvz * v.z; + + return mtx; +} + +float LinearizeDepth(const float screenDepth) +{ + float depthLinearizeMul = u_Camera.DepthUnpackConsts.x; + float depthLinearizeAdd = u_Camera.DepthUnpackConsts.y; + // Optimised version of "-cameraClipNear / (cameraClipFar - projDepth * (cameraClipFar - cameraClipNear)) * cameraClipFar" + return depthLinearizeMul / (depthLinearizeAdd - screenDepth); +} + +float4 LinearizeDepth(const float4 screenDepths) +{ + return float4(LinearizeDepth(screenDepths.x), LinearizeDepth(screenDepths.y), LinearizeDepth(screenDepths.z), LinearizeDepth(screenDepths.w)); +} + +void XeGTAO_MainPass(const int2 outputPixCoord, const int2 inputPixCoords, lpfloat sliceCount, lpfloat stepsPerSlice, const lpfloat2 localNoise) +{ + lpfloat4 viewspaceNormalLuminance = (lpfloat4)u_ViewNormal.Load(int3(inputPixCoords, 0)); + lpfloat3 viewspaceNormal = viewspaceNormalLuminance.xyz; + viewspaceNormal.yz = -viewspaceNormalLuminance.yz; + + + float2 normalizedScreenPos = (inputPixCoords + 0.5.xx) * u_ScreenData.InvFullResolution; + + float4 deviceZs = u_HiZDepth.GatherRed(u_samplerPointClamp, float2(inputPixCoords * u_ScreenData.InvFullResolution * u_GTAOConsts.HZBUVFactor)); + lpfloat4 valuesUL = (lpfloat4)LinearizeDepth(deviceZs); + lpfloat4 valuesBR = (lpfloat4)LinearizeDepth(u_HiZDepth.GatherRed(u_samplerPointClamp, float2(inputPixCoords * u_ScreenData.InvFullResolution * u_GTAOConsts.HZBUVFactor), int2(1, 1))); + + // viewspace Z at the center + lpfloat viewspaceZ = valuesUL.y; //u_HiZDepth.SampleLevel( u_samplerPointClamp, normalizedScreenPos, 0 ).x; + + // viewspace Zs left top right bottom + const lpfloat pixLZ = valuesUL.x; + const lpfloat pixTZ = valuesUL.z; + const lpfloat pixRZ = valuesBR.z; + const lpfloat pixBZ = valuesBR.x; + + lpfloat4 edgesLRTB = XeGTAO_CalculateEdges((lpfloat)viewspaceZ, (lpfloat)pixLZ, (lpfloat)pixRZ, (lpfloat)pixTZ, (lpfloat)pixBZ); + o_Edges[outputPixCoord] = XeGTAO_PackEdges(edgesLRTB); + + // Move center pixel slightly towards camera to avoid imprecision artifacts due to depth buffer imprecision; offset depends on depth texture format used +#ifdef XE_GTAO_FP32_DEPTHS + viewspaceZ *= 0.99999; // this is good for FP32 depth buffer +#else + viewspaceZ *= 0.99920; // this is good for FP16 depth buffer +#endif + + const float3 pixCenterPos = XeGTAO_ComputeViewspacePosition(normalizedScreenPos, viewspaceZ); + const lpfloat3 viewVec = (lpfloat3)normalize(-pixCenterPos); + + // prevents normals that are facing away from the view vector - xeGTAO struggles with extreme cases, but in Vanilla it seems rare so it's disabled by default + // viewspaceNormal = normalize( viewspaceNormal + max( 0, -dot( viewspaceNormal, viewVec ) ) * viewVec ); + +#if XE_GTAO_USE_DEFAULT_CONSTANTS + const lpfloat effectRadius = (lpfloat)u_GTAOConsts.EffectRadius * (lpfloat)XE_GTAO_DEFAULT_RADIUS_MULTIPLIER; + const lpfloat sampleDistributionPower = (lpfloat)XE_GTAO_DEFAULT_SAMPLE_DISTRIBUTION_POWER; + const lpfloat thinOccluderCompensation = (lpfloat)XE_GTAO_DEFAULT_THIN_OCCLUDER_COMPENSATION; + const lpfloat falloffRange = (lpfloat)XE_GTAO_DEFAULT_FALLOFF_RANGE * effectRadius; +#else + const lpfloat effectRadius = (lpfloat)u_GTAOConsts.EffectRadius * (lpfloat)u_GTAOConsts.RadiusMultiplier; + const lpfloat sampleDistributionPower = (lpfloat)u_GTAOConsts.SampleDistributionPower; + const lpfloat thinOccluderCompensation = (lpfloat)u_GTAOConsts.ThinOccluderCompensation; + const lpfloat falloffRange = (lpfloat)u_GTAOConsts.EffectFalloffRange * effectRadius; +#endif + + const lpfloat falloffFrom = effectRadius * ((lpfloat)1 - (lpfloat)u_GTAOConsts.EffectFalloffRange); + + // fadeout precompute optimisation + const lpfloat falloffMul = (lpfloat)-1.0 / (falloffRange); + const lpfloat falloffAdd = falloffFrom / (falloffRange)+(lpfloat)1.0; + + lpfloat visibility = 0; +#if __HZ_GTAO_COMPUTE_BENT_NORMALS + lpfloat3 bentNormal = 0; +#else + lpfloat3 bentNormal = viewspaceNormal; +#endif + + // see "Algorithm 1" in https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf + { + const lpfloat noiseSlice = (lpfloat)localNoise.x; + const lpfloat noiseSample = (lpfloat)localNoise.y; + + // quality settings / tweaks / hacks + const lpfloat pixelTooCloseThreshold = 1.3; // if the offset is under approx pixel size (pixelTooCloseThreshold), push it out to the minimum distance + + // approx viewspace pixel size at inputPixCoords; approximation of NDCToViewspace( normalizedScreenPos.xy + u_ScreenData.InvFullResolution.xy, pixCenterPos.z ).xy - pixCenterPos.xy; + const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * u_GTAOConsts.NDCToViewMul_x_PixelSize; + + lpfloat screenspaceRadius = effectRadius / (lpfloat)pixelDirRBViewspaceSizeAtCenterZ.x; + + // fade out for small screen radii + visibility += saturate((10 - screenspaceRadius) / 100) * 0.5; + +#if 1 // sensible early-out for even more performance; disabled because not yet tested + lpfloat normals = (lpfloat)viewspaceNormal; + [branch] + if (!deviceZs.y || screenspaceRadius < pixelTooCloseThreshold) + { + XeGTAO_OutputWorkingTerm(outputPixCoord, 1, viewspaceNormal, o_AOwBentNormals); + return; + } +#endif + + // this is the min distance to start sampling from to avoid sampling from the center pixel (no useful data obtained from sampling center pixel) + const lpfloat minS = (lpfloat)pixelTooCloseThreshold / screenspaceRadius; + + [unroll] + for (lpfloat slice = 0; slice < sliceCount; slice++) + { + lpfloat sliceK = (slice + noiseSlice) / sliceCount; + // lines 5, 6 from the paper + lpfloat phi = sliceK * XE_GTAO_PI; + lpfloat cosPhi = cos(phi); + lpfloat sinPhi = sin(phi); + lpfloat2 omega = lpfloat2(cosPhi, -sinPhi); //lpfloat2 on omega causes issues with big radii + + // convert to screen units (pixels) for later use + omega *= screenspaceRadius; + + // line 8 from the paper + const lpfloat3 directionVec = lpfloat3(cosPhi, sinPhi, 0); + + // line 9 from the paper + const lpfloat3 orthoDirectionVec = directionVec - (dot(directionVec, viewVec) * viewVec); + + // line 10 from the paper + //axisVec is orthogonal to directionVec and viewVec, used to define projectedNormal + const lpfloat3 axisVec = normalize(cross(orthoDirectionVec, viewVec)); + + // alternative line 9 from the paper + // float3 orthoDirectionVec = cross( viewVec, axisVec ); + + // line 11 from the paper + lpfloat3 projectedNormalVec = viewspaceNormal - axisVec * dot(viewspaceNormal, axisVec); + + // line 13 from the paper + lpfloat signNorm = (lpfloat)sign(dot(orthoDirectionVec, projectedNormalVec)); + + // line 14 from the paper + lpfloat projectedNormalVecLength = length(projectedNormalVec); + lpfloat cosNorm = (lpfloat)saturate(dot(projectedNormalVec, viewVec) / projectedNormalVecLength); + + // line 15 from the paper + lpfloat n = signNorm * XeGTAO_FastACos(cosNorm); + + // this is a lower weight target; not using -1 as in the original paper because it is under horizon, so a 'weight' has different meaning based on the normal + const lpfloat lowHorizonCos0 = cos(n + XE_GTAO_PI_HALF); + const lpfloat lowHorizonCos1 = cos(n - XE_GTAO_PI_HALF); + + // lines 17, 18 from the paper, manually unrolled the 'side' loop + lpfloat horizonCos0 = lowHorizonCos0; //-1; + lpfloat horizonCos1 = lowHorizonCos1; //-1; + + [unroll] + for (lpfloat step = 0; step < stepsPerSlice; step++) + { + // R1 sequence (http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) + const lpfloat stepBaseNoise = lpfloat(slice + step * stepsPerSlice) * 0.6180339887498948482; // <- this should unroll + lpfloat stepNoise = frac(noiseSample + stepBaseNoise); + + // approx line 20 from the paper, with added noise + lpfloat s = (step + stepNoise) / (stepsPerSlice); // + (lpfloat2)1e-6f); + + // additional distribution modifier + s = (lpfloat)pow(s, (lpfloat)sampleDistributionPower); + + // avoid sampling center pixel + s += minS; + + // approx lines 21-22 from the paper, unrolled + lpfloat2 sampleOffset = s * omega; + + lpfloat sampleOffsetLength = length(sampleOffset); + + // note: when sampling, using point_point_point or point_point_linear sampler works, but linear_linear_linear will cause unwanted interpolation between neighbouring depth values on the same MIP level! + const lpfloat mipLevel = (lpfloat)clamp(log2(sampleOffsetLength) - u_GTAOConsts.DepthMIPSamplingOffset, 0, XE_GTAO_DEPTH_MIP_LEVELS); + + // Snap to pixel center (more correct direction math, avoids artifacts due to sampling pos not matching depth texel center - messes up slope - but adds other + // artifacts due to them being pushed off the slice). Also use full precision for high res cases. + sampleOffset = round(sampleOffset) * (lpfloat2)u_ScreenData.InvFullResolution; + + float2 sampleScreenPos0 = normalizedScreenPos + sampleOffset; + float SZ0 = LinearizeDepth(u_HiZDepth.SampleLevel(u_samplerPointClamp, sampleScreenPos0 * u_GTAOConsts.HZBUVFactor, mipLevel).x); + float3 samplePos0 = XeGTAO_ComputeViewspacePosition(sampleScreenPos0, SZ0); + + float2 sampleScreenPos1 = normalizedScreenPos - sampleOffset; + float SZ1 = LinearizeDepth(u_HiZDepth.SampleLevel(u_samplerPointClamp, sampleScreenPos1 * u_GTAOConsts.HZBUVFactor, mipLevel).x); + float3 samplePos1 = XeGTAO_ComputeViewspacePosition(sampleScreenPos1, SZ1); + + float3 sampleDelta0 = (samplePos0 - float3(pixCenterPos)); // using lpfloat for sampleDelta causes precision issues + float3 sampleDelta1 = (samplePos1 - float3(pixCenterPos)); // using lpfloat for sampleDelta causes precision issues + lpfloat sampleDist0 = (lpfloat)length(sampleDelta0); + lpfloat sampleDist1 = (lpfloat)length(sampleDelta1); + + // approx lines 23, 24 from the paper, unrolled + lpfloat3 sampleHorizonVec0 = (lpfloat3)(sampleDelta0 / sampleDist0); + lpfloat3 sampleHorizonVec1 = (lpfloat3)(sampleDelta1 / sampleDist1); + + // any sample out of radius should be discarded - also use fallof range for smooth transitions; this is a modified idea from "4.3 Implementation details, Bounding the sampling area" +#if XE_GTAO_USE_DEFAULT_CONSTANTS != 0 && XE_GTAO_DEFAULT_THIN_OBJECT_HEURISTIC == 0 + lpfloat weight0 = saturate(sampleDist0 * falloffMul + falloffAdd); + lpfloat weight1 = saturate(sampleDist1 * falloffMul + falloffAdd); +#else + // this is our own thickness heuristic that relies on sooner discarding samples behind the center + lpfloat falloffBase0 = length(lpfloat3(sampleDelta0.x, sampleDelta0.y, sampleDelta0.z * (1 + thinOccluderCompensation))); + lpfloat falloffBase1 = length(lpfloat3(sampleDelta1.x, sampleDelta1.y, sampleDelta1.z * (1 + thinOccluderCompensation))); + lpfloat weight0 = saturate(falloffBase0 * falloffMul + falloffAdd); + lpfloat weight1 = saturate(falloffBase1 * falloffMul + falloffAdd); +#endif + + // sample horizon cos + lpfloat shc0 = (lpfloat)dot(sampleHorizonVec0, viewVec); + lpfloat shc1 = (lpfloat)dot(sampleHorizonVec1, viewVec); + + // discard unwanted samples + shc0 = lerp(lowHorizonCos0, shc0, weight0); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos0), acos(shc0), weight0 )); + shc1 = lerp(lowHorizonCos1, shc1, weight1); // this would be more correct but too expensive: cos(lerp( acos(lowHorizonCos1), acos(shc1), weight1 )); + + // thickness heuristic - see "4.3 Implementation details, Height-field assumption considerations" +#if 0 // (disabled, not used) this should match the paper + lpfloat newhorizonCos0 = max(horizonCos0, shc0); + lpfloat newhorizonCos1 = max(horizonCos1, shc1); + horizonCos0 = (horizonCos0 > shc0) ? (lerp(newhorizonCos0, shc0, thinOccluderCompensation)) : (newhorizonCos0); + horizonCos1 = (horizonCos1 > shc1) ? (lerp(newhorizonCos1, shc1, thinOccluderCompensation)) : (newhorizonCos1); +#elif 0 // (disabled, not used) this is slightly different from the paper but cheaper and provides very similar results + horizonCos0 = lerp(max(horizonCos0, shc0), shc0, thinOccluderCompensation); + horizonCos1 = lerp(max(horizonCos1, shc1), shc1, thinOccluderCompensation); +#else // this is a version where thicknessHeuristic is completely disabled + horizonCos0 = max(horizonCos0, shc0); + horizonCos1 = max(horizonCos1, shc1); +#endif + } + +#if 1 // I can't figure out the slight overdarkening on high slopes, so I'm adding this fudge - in the training set, 0.05 is close (PSNR 21.34) to disabled (PSNR 21.45) + projectedNormalVecLength = lerp(projectedNormalVecLength, 1, 0.05); +#endif + + // line ~27, unrolled + lpfloat h0 = -XeGTAO_FastACos((lpfloat)horizonCos1); + lpfloat h1 = XeGTAO_FastACos((lpfloat)horizonCos0); +#if 0 // we can skip clamping for a tiny little bit more performance + h0 = n + clamp(h0 - n, (lpfloat)-XE_GTAO_PI_HALF, (lpfloat)XE_GTAO_PI_HALF); + h1 = n + clamp(h1 - n, (lpfloat)-XE_GTAO_PI_HALF, (lpfloat)XE_GTAO_PI_HALF); +#endif + lpfloat iarc0 = ((lpfloat)cosNorm + (lpfloat)2 * (lpfloat)h0 * (lpfloat)sin(n) - (lpfloat)cos((lpfloat)2 * (lpfloat)h0 - n)) / (lpfloat)4; + lpfloat iarc1 = ((lpfloat)cosNorm + (lpfloat)2 * (lpfloat)h1 * (lpfloat)sin(n) - (lpfloat)cos((lpfloat)2 * (lpfloat)h1 - n)) / (lpfloat)4; + lpfloat localVisibility = (lpfloat)projectedNormalVecLength * (lpfloat)(iarc0 + iarc1); + visibility += localVisibility; + +#if __HZ_GTAO_COMPUTE_BENT_NORMALS + // see "Algorithm 2 Extension that computes bent normals b." + lpfloat t0 = (6 * sin(h0 - n) - sin(3 * h0 - n) + 6 * sin(h1 - n) - sin(3 * h1 - n) + 16 * sin(n) - 3 * (sin(h0 + n) + sin(h1 + n))) / 12; + lpfloat t1 = (-cos(3 * h0 - n) - cos(3 * h1 - n) + 8 * cos(n) - 3 * (cos(h0 + n) + cos(h1 + n))) / 12; + lpfloat3 localBentNormal = lpfloat3(directionVec.x * (lpfloat)t0, directionVec.y * (lpfloat)t0, -lpfloat(t1)); + localBentNormal = (lpfloat3)mul(XeGTAO_RotFromToMatrix(lpfloat3(0, 0, -1), viewVec), localBentNormal) * projectedNormalVecLength; + bentNormal += localBentNormal; +#endif + } + visibility /= (lpfloat)sliceCount; + visibility = (lpfloat)pow(visibility, (lpfloat)u_GTAOConsts.FinalValuePower * lerp(1.0f, (lpfloat)u_GTAOConsts.ShadowTolerance, viewspaceNormalLuminance.a)); + visibility = max((lpfloat)0.03, visibility); // disallow total occlusion (which wouldn't make any sense anyhow since pixel is visible but also helps with packing bent normals) + +#if __HZ_GTAO_COMPUTE_BENT_NORMALS + bentNormal = normalize(bentNormal); +#endif + } + + XeGTAO_OutputWorkingTerm(outputPixCoord, visibility, bentNormal, o_AOwBentNormals); +} + +// Engine-specific screen & temporal noise loader +lpfloat2 SpatioTemporalNoise(uint2 pixCoord, uint temporalIndex) // without TAA, temporalIndex is always 0 +{ + float2 noise; + // Hilbert curve driving R2 (see https://www.shadertoy.com/view/3tB3z3) + uint index = u_HilbertLut.Load(uint3(pixCoord % 64, 0)).x; + index += 288 * (temporalIndex % 64); // why 288? tried out a few and that's the best so far (with XE_HILBERT_LEVEL 6U) - but there's probably better :) + // R2 sequence - see http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ + return lpfloat2(frac(0.5 + index * float2(0.75487766624669276005, 0.5698402909980532659114))); +} + +// Engine-specific entry point for the second pass +[numthreads(XE_GTAO_NUMTHREADS_X, XE_GTAO_NUMTHREADS_Y, 1)] +void main(const uint2 pixCoord : SV_DispatchThreadID) +{ + const int2 outputPixCoords = pixCoord; + const int2 inputPixCoords = outputPixCoords * (1 + int(u_GTAOConsts.HalfRes)); + XeGTAO_MainPass(outputPixCoords, inputPixCoords, 9, 3, SpatioTemporalNoise(inputPixCoords, u_GTAOConsts.NoiseIndex)); +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/PostProcessing/Pre-Convolution.glsl b/StarEditor/Resources/Shaders/PostProcessing/Pre-Convolution.glsl new file mode 100644 index 00000000..275c9636 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/Pre-Convolution.glsl @@ -0,0 +1,76 @@ +#version 430 core +#pragma stage : comp + +layout(binding = 0, rgba32f) restrict writeonly uniform image2D o_Image; +layout(binding = 1) uniform sampler2D u_Input; + +layout(push_constant) uniform Uniforms +{ + int PrevLod; + int Mode; //See defines below +} u_Info; + +#define MODE_COPY (0) +#define MODE_HORIZONTAL_GAUSSIAN (1) +#define MODE_VERTICAL_GAUSSIAN (2) + +#define WEIGHT0 0.1749379741597446 +#define WEIGHT1 0.16556904917484133 +#define WEIGHT2 0.14036678002195038 +#define WEIGHT3 0.106595183723336 + +vec4 GaussianHorizontal(vec2 uv, vec2 pixelSize) +{ + const float lod = u_Info.PrevLod; + vec4 color = textureLod(u_Input, uv + pixelSize.x * vec2(-3.0f, 0.0f), lod) * WEIGHT3; + color += textureLod(u_Input, uv + pixelSize.x * vec2(-2.0f, 0.0f), lod) * WEIGHT2; + color += textureLod(u_Input, uv + pixelSize.x * vec2(-1.0f, 0.0f), lod) * WEIGHT1; + color += textureLod(u_Input, uv, lod) * WEIGHT0; + color += textureLod(u_Input, uv + pixelSize.x * vec2( 1.0f, 0.0f), lod) * WEIGHT1; + color += textureLod(u_Input, uv + pixelSize.x * vec2( 2.0f, 0.0f), lod) * WEIGHT2; + color += textureLod(u_Input, uv + pixelSize.x * vec2( 3.0f, 0.0f), lod) * WEIGHT3; + + return color; +} + +vec4 GaussianVertical(vec2 uv, vec2 pixelSize) +{ + const float lod = u_Info.PrevLod + 1; + vec4 color = textureLod(u_Input, uv + pixelSize.y * vec2(0.0f, -3.0f), lod) * WEIGHT3; + color += textureLod(u_Input, uv + pixelSize.y * vec2(0.0f, -2.0f), lod) * WEIGHT2; + color += textureLod(u_Input, uv + pixelSize.y * vec2(0.0f, -1.0f), lod) * WEIGHT1; + color += textureLod(u_Input, uv, lod) * WEIGHT0; + color += textureLod(u_Input, uv + pixelSize.y * vec2(0.0f, 1.0f), lod) * WEIGHT1; + color += textureLod(u_Input, uv + pixelSize.y * vec2(0.0f, 2.0f), lod) * WEIGHT2; + color += textureLod(u_Input, uv + pixelSize.y * vec2(0.0f, 3.0f), lod) * WEIGHT3; + return color; +} + + +layout(local_size_x = 16, local_size_y = 16) in; +void main() +{ + vec2 imgSize = imageSize(o_Image); + ivec2 invocID = ivec2(gl_GlobalInvocationID); + vec2 pixelSize = 1.0f / imgSize; + vec2 texCoords = invocID * pixelSize + pixelSize * 0.5; + vec4 finalColor; + if (u_Info.Mode == MODE_COPY) + { + finalColor = texture(u_Input, texCoords); + } + else if (u_Info.Mode == MODE_HORIZONTAL_GAUSSIAN) + { + finalColor = GaussianHorizontal(texCoords, pixelSize); + } + else if(u_Info.Mode == MODE_VERTICAL_GAUSSIAN) + { + finalColor = GaussianVertical(texCoords, pixelSize); + } + + imageStore(o_Image, invocID, finalColor); +} + + + + diff --git a/StarEditor/Resources/Shaders/PostProcessing/SSR-Composite.glsl b/StarEditor/Resources/Shaders/PostProcessing/SSR-Composite.glsl new file mode 100644 index 00000000..5ed65602 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/SSR-Composite.glsl @@ -0,0 +1,26 @@ +#version 430 core +#pragma stage:vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +layout(location = 0) out vec2 vs_TexCoords; + +void main() +{ + vs_TexCoords = a_TexCoord; + gl_Position = vec4(a_Position.xy, 0.0, 1.0); +} + +#version 450 core +#pragma stage:frag + +layout(set = 1, binding = 0) uniform sampler2D u_SSR; + +layout(location = 0) out vec4 outColor; +layout(location = 0) in vec2 vs_TexCoords; + +void main() +{ + outColor = texture(u_SSR, vs_TexCoords); +} diff --git a/StarEditor/Resources/Shaders/PostProcessing/SSR.glsl b/StarEditor/Resources/Shaders/PostProcessing/SSR.glsl new file mode 100644 index 00000000..ee3a3381 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/SSR.glsl @@ -0,0 +1,400 @@ +// --------------------------------------- +// -- Hazel Engine SSR shader -- +// --------------------------------------- +// - References: +// AMD's Stochastic screen-space reflections https://gpuopen.com/fidelityfx-sssr/ +// GPU Pro 5 book chapter 4 by Yasin Uludag +// Will Pearce's blog http://roar11.com/2015/07/screen-space-glossy-reflections/ +// Unreal Engine 4 SSR implimentation +// Lukas Hermanns's thesis https://lukas-hermanns.info/download/bachelorthesis_ssct_lhermanns.pdf +#version 430 core +#pragma stage : comp +#include +#include + +#include + +layout(set = 1, binding = 1, rgba16f) restrict writeonly uniform image2D outColor; +layout(set = 1, binding = 2) uniform sampler2D u_InputColor; +layout(set = 1, binding = 3) uniform sampler2D u_Normal; +layout(set = 1, binding = 4) uniform sampler2D u_HiZBuffer; +layout(set = 1, binding = 5) uniform sampler2D u_MetalnessRoughness; +layout(set = 1, binding = 6) uniform sampler2D u_VisibilityBuffer; + +#define GTAO_REFLECTION_OCCLUSION (__HZ_REFLECTION_OCCLUSION_METHOD & HZ_REFLECTION_OCCLUSION_METHOD_GTAO) +#define HBAO_REFLECTION_OCCLUSION (__HZ_REFLECTION_OCCLUSION_METHOD & HZ_REFLECTION_OCCLUSION_METHOD_HBAO) + +#if GTAO_REFLECTION_OCCLUSION + layout(set = 1, binding = 7) uniform usampler2D u_GTAOTex; +#endif + +#if HBAO_REFLECTION_OCCLUSION + layout(set = 1, binding = 8) uniform sampler2D u_HBAOTex; +#endif + +layout(push_constant) uniform SSRInfo +{ + vec2 HZBUVFactor; + vec2 FadeIn; + float Brightness; + float DepthTolerance; + float FacingReflectionsFading; + int MaxSteps; + uint NumDepthMips; + float RoughnessDepthTolerance; // The higher the roughness the more we have depth tolerance + bool HalfRes; + bool EnableConeTracing; + float LuminanceFactor; +} u_SSRInfo; + + +#define INVERTED_DEPTH_RANGE +const float M_PI = 3.14159265359f; +const float FLOAT_MAX = 3.402823466e+38f; +const int BASE_LOD = 0; + +/////////////////////////////////////////////////////////////////////////////////////// +// Hi-Z cone tracing +/////////////////////////////////////////////////////////////////////////////////////// + +float IsoscelesTriangleOpposite(float adjacentLength, float coneTheta) +{ + return 2.0 * tan(coneTheta) * adjacentLength; +} + +float IsoscelesTriangleInRadius(float a, float h) +{ + float a2 = a * a; + float fh2 = 4.0f * h * h; + return (a * (sqrt(a2 + fh2) - a)) / (4.0f * h); +} + +vec4 ConeSampleWeightedColor(vec2 samplePos, float mipLevel) +{ + /* Sample color buffer with pre-integrated visibility */ + vec3 color = textureLod(u_InputColor, samplePos, mipLevel).rgb; + float visibility = textureLod(u_VisibilityBuffer, samplePos, mipLevel).r; + return vec4(color * visibility, visibility); +} + +float IsoscelesTriangleNextAdjacent(float adjacentLength, float incircleRadius) +{ + // subtract the diameter of the incircle to get the adjacent side of the next level on the cone + return adjacentLength - (incircleRadius * 2.0); +} + +vec4 ConeTracing(float roughness, vec2 rayOriginSS, vec2 rayPosSS) +{ + float coneTheta = roughness * M_PI * 0.025; + + vec2 res = u_ScreenData.FullResolution * (1 + int(!u_SSRInfo.HalfRes)); + + /* Cone tracing using an isosceles triangle to approximate a cone in screen space */ + vec2 deltaPos = rayPosSS - rayOriginSS; + + + float adjacentLength = length(deltaPos); + vec2 adjacentUnit = normalize(deltaPos); + + vec4 reflectionColor = vec4(0.0); + vec2 samplePos; + float remainingAlpha = 1.0f; + for (int i = 0; i < 7; ++i) + { + // intersection length is the adjacent side, get the opposite side using trig + float oppositeLength = IsoscelesTriangleOpposite(adjacentLength, coneTheta); + + // calculate in-radius of the isosceles triangle + float incircleSize = IsoscelesTriangleInRadius(oppositeLength, adjacentLength); + + // get the sample position in screen space + samplePos = rayOriginSS + adjacentUnit * (adjacentLength - incircleSize); + + // convert the in-radius into screen size then check what power N to raise 2 to reach it - that power N becomes mip level to sample from + float mipChannel = clamp(log2(incircleSize * max(res.x, res.y)), 0.0f, u_SSRInfo.NumDepthMips); + + /* + * Read color and accumulate it using trilinear filtering and weight it. + * Uses pre-convolved image (color buffer) and glossiness to weigh color contributions. + * Visibility is accumulated in the alpha channel. Break if visibility is 100% or greater (>= 1.0f). + */ + vec4 newColor = ConeSampleWeightedColor(samplePos.xy, mipChannel); + remainingAlpha -= newColor.a; + if (remainingAlpha < 0.0f) + { + newColor.rgb *= (1.0f - abs(remainingAlpha)); + } + reflectionColor += newColor; + + if (reflectionColor.a >= 1.0f) + { + break; + } + + adjacentLength = IsoscelesTriangleNextAdjacent(adjacentLength, incircleSize); + } + + return reflectionColor; +} + +/////////////////////////////////////////////////////////////////////////////////////// +// Hi-Z ray tracing methods +/////////////////////////////////////////////////////////////////////////////////////// + +float FetchDepth(ivec2 coords, int lod) +{ + return texelFetch(u_HiZBuffer, coords, lod).x; +} + +ivec2 GetDepthMipResolution(int mipLevel) +{ + return ivec2(textureSize(u_HiZBuffer, mipLevel) * u_SSRInfo.HZBUVFactor); +} + +// Inputs are screen XY and viewspace depth, output is viewspace position +// From XeGTAO +vec3 ComputeViewspacePosition(const vec2 screenPos, const float viewspaceDepth) +{ + vec3 ret; + ret.xy = (u_Camera.NDCToViewMul * screenPos.xy + u_Camera.NDCToViewAdd) * viewspaceDepth; + ret.z = -viewspaceDepth; + return ret; +} + +//// From XeGTAO +float ScreenSpaceToViewSpaceDepth(const float screenDepth) +{ + float depthLinearizeMul = u_Camera.DepthUnpackConsts.x; + float depthLinearizeAdd = u_Camera.DepthUnpackConsts.y; + // Optimised version of "-cameraClipNear / (cameraClipFar - projDepth * (cameraClipFar - cameraClipNear)) * cameraClipFar" + return depthLinearizeMul / (depthLinearizeAdd - screenDepth); +} + +void InitialAdvanceRay(vec3 origin, vec3 direction, vec3 invDirection, vec2 currentMipResolution, vec2 currentMipResolutionInv, vec2 floorOffset, vec2 uvOffset, out vec3 position, out float currentT) +{ + vec2 currentMipPosition = currentMipResolution * origin.xy; + + // Intersect ray with the half box that is pointing away from the ray origin. + vec2 xyPlane = floor(currentMipPosition) + floorOffset; + xyPlane = xyPlane * currentMipResolutionInv + uvOffset; + + // o + d * t = p' => t = (p' - o) / d + vec2 t = xyPlane * invDirection.xy - origin.xy * invDirection.xy; + currentT = min(t.x, t.y); + position = origin + currentT * direction; +} + +void SecondAdvanceRay(vec3 origin, vec3 direction, vec3 invDirection, vec2 currentMipPosition, vec2 currentMipResolutionInv, vec2 floorOffset, vec2 uvOffset, inout vec3 position, inout float currentT) +{ + // Create boundary planes + vec2 xyPlane = floor(currentMipPosition) + floorOffset; + xyPlane = xyPlane * currentMipResolutionInv + uvOffset; + + // o + d * t = p' => t = (p' - o) / d + vec2 t = xyPlane * invDirection.xy - origin.xy * invDirection.xy; + currentT = min(t.x, t.y); + position = origin + currentT * direction; +} + +bool AdvanceRay(vec3 origin, vec3 direction, vec3 invDirection, vec2 currentMipPosition, vec2 currentMipResolutionInv, vec2 floorOffset, vec2 uvOffset, float surfaceZ, inout vec3 position, inout float currentT) +{ + // Create boundary planes + vec2 xyPlane = floor(currentMipPosition) + floorOffset; + xyPlane = xyPlane * currentMipResolutionInv + uvOffset; + vec3 boundaryPlanes = vec3(xyPlane, surfaceZ); + + // Intersect ray with the half box that is pointing away from the ray origin. + // o + d * t = p' => t = (p' - o) / d + vec3 t = boundaryPlanes * invDirection - origin * invDirection; + + // Prevent using z plane when shooting out of the depth buffer. + #ifdef INVERTED_DEPTH_RANGE + t.z = direction.z < 0 ? t.z : FLOAT_MAX; + #else + t.z = direction.z > 0 ? t.z : FLOAT_MAX; + #endif + + // Choose nearest intersection with a boundary. + float tMin = min(min(t.x, t.y), t.z); + + #ifdef INVERTED_DEPTH_RANGE + // Larger z means closer to the camera. + bool aboveSurface = surfaceZ < position.z; + #else + // Smaller z means closer to the camera. + bool aboveSurface = surfaceZ > position.z; + #endif + + // Decide whether we are able to advance the ray until we hit the xy boundaries or if we had to clamp it at the surface. + // We use the asuint comparison to avoid NaN / Inf logic, also we actually care about bitwise equality here to see if t_min is the t.z we fed into the min3 above. + bool skippedTile = floatBitsToUint(tMin) != floatBitsToUint(t.z) && aboveSurface; + + // Make sure to only advance the ray if we're still above the surface. + currentT = aboveSurface ? tMin : currentT; + + // Advance ray + position = origin + currentT * direction; + + return skippedTile; +} + +// Requires origin and direction of the ray to be in screen space [0, 1] x [0, 1] +vec3 HierarchicalRaymarch(vec3 origin, vec3 direction, vec2 screenSize, int mostDetailedMip, uint minTraversalOccupancy, uint maxTraversalIntersections, out uint iterations, out bool validHit) +{ + const vec3 invDirection = direction != vec3(0) ? 1.0 / direction : vec3(FLOAT_MAX); + + // Start on mip with highest detail. + int currentMip = mostDetailedMip; + + // Could recompute these every iteration, but it's faster to hoist them out and update them. + vec2 currentMipResolution = GetDepthMipResolution(currentMip); + vec2 currentMipResolutionInv = 1 / currentMipResolution; + + // Offset to the bounding boxes uv space to intersect the ray with the center of the next pixel. + // This means we ever so slightly over shoot into the next region. + vec2 uvOffset = 0.005 * exp2(mostDetailedMip) / screenSize; + uvOffset.x = (direction.x < 0.0 ? -uvOffset.x : uvOffset.x); + uvOffset.y = (direction.y < 0.0 ? -uvOffset.y : uvOffset.y); + + // Offset applied depending on current mip resolution to move the boundary to the left/right upper/lower border depending on ray direction. + vec2 floorOffset; + floorOffset.x = (direction.x < 0.0 ? 0.0 : 1.0); + floorOffset.y = (direction.y < 0.0 ? 0.0 : 1.0); + + float currentT; + vec3 position; + // Initially advance ray to avoid immediate self intersections. + InitialAdvanceRay(origin, direction, invDirection, currentMipResolution, currentMipResolutionInv, floorOffset, uvOffset, position, currentT); + vec2 currentMipPosition = currentMipResolution * position.xy; + + // This is a way to prevent artifacts on bumpy surfaces (rough normals). + SecondAdvanceRay(origin, direction, invDirection, currentMipPosition, currentMipResolutionInv, floorOffset, uvOffset, position, currentT); + + iterations = 0; + while (iterations < maxTraversalIntersections && currentMip >= mostDetailedMip) + { + currentMipPosition = currentMipResolution * position.xy; + float surfaceZ = FetchDepth(ivec2(currentMipPosition), currentMip); + bool skippedTile = AdvanceRay(origin, direction, invDirection, currentMipPosition, currentMipResolutionInv, floorOffset, uvOffset, surfaceZ, position, currentT); + currentMip += skippedTile ? 1 : -1; + currentMipResolution *= skippedTile ? 0.5 : 2; + currentMipResolutionInv *= skippedTile ? 2 : 0.5; + ++iterations; + } + + validHit = (iterations < maxTraversalIntersections); + + return position; +} + +float ValidateHit(vec3 origin, vec3 ray, vec3 originPosVS, vec3 rayDirVS, float roughness) +{ + // Reject hits outside the view frustum + if (any(lessThan(ray.xy, vec2(0.0))) || any(greaterThan(ray.xy, vec2(1.0)))) + return 0; + + // Reject the ray if we didnt advance the ray significantly to avoid immediate self reflection + vec2 manhattanDist = abs(ray.xy - origin.xy); + if (all(lessThan(manhattanDist, u_ScreenData.InvHalfResolution))) + return 0; + + // Don't lookup radiance from the background. + float surfaceZ = FetchDepth(ivec2(GetDepthMipResolution(0) * ray.xy), 0).x; +#ifdef INVERTED_DEPTH_RANGE + if (surfaceZ == 0.0) +#else + if (surfaceZ == 1.0) +#endif + return 0; + + float surfaceDepthVS = ScreenSpaceToViewSpaceDepth(surfaceZ); + vec3 surfacePosVS = ComputeViewspacePosition(ray.xy, surfaceDepthVS); + + float hitDepthVS = ScreenSpaceToViewSpaceDepth(ray.z); + vec3 hitPositionVS = ComputeViewspacePosition(ray.xy, hitDepthVS); + + float distanceTravelled = length(surfacePosVS - hitPositionVS); + + // We accept all hits that are within a reasonable minimum distance below the surface. + // Add constant in linear space to avoid growing of the reflections toward the reflected objects. + // Roughness depth tolerance allows rough surfaces to be more depth tolerant, meaning with less failing rays. + float confidence = 1.0f - smoothstep(0.0f, u_SSRInfo.DepthTolerance + mix(0.0f, u_SSRInfo.RoughnessDepthTolerance, roughness), distanceTravelled); + + // Fade out hits near the screen borders + vec2 viewportSize = u_ScreenData.FullResolution; + vec2 fov = u_SSRInfo.FadeIn * vec2(viewportSize.y / viewportSize.x, 1.0f); + vec2 border = smoothstep(vec2(0.0f), fov, ray.xy) * (1.0f - smoothstep(1.0f - fov, vec2(1.0f), ray.xy)); + float vignette = border.x * border.y; + + // Fade rays pointing towards camera + float fadeOnMirror = clamp(max(dot(originPosVS, rayDirVS), 0.0f) + u_SSRInfo.FacingReflectionsFading, 0.0f, 1.0f); + return confidence * fadeOnMirror * vignette * (1.0f - roughness); +} + +vec3 FresnelSchlickRoughness(vec3 F0, float cosTheta, float roughness) +{ + return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); +} + +layout(local_size_x = 8, local_size_y = 8) in; +void main() +{ + const ivec2 base = ivec2(gl_GlobalInvocationID); + const vec2 uv = u_SSRInfo.HalfRes ? u_ScreenData.InvHalfResolution * (base + 0.25) : u_ScreenData.InvFullResolution * (base + 0.50); + const ivec2 baseDepthResolution = GetDepthMipResolution(BASE_LOD); + const float depth = FetchDepth(ivec2(baseDepthResolution * uv), 0).r; + + const vec4 albedo = texture(u_InputColor, uv); + const vec2 metalnessRoughness = texture(u_MetalnessRoughness, uv).xy; + const vec3 normalVS = texture(u_Normal, uv).xyz; + const float depthVS = ScreenSpaceToViewSpaceDepth(depth); + const vec3 positionVS = ComputeViewspacePosition(uv, depthVS); + const vec3 toPositionVS = normalize(positionVS.xyz); + const vec3 reflectVS = reflect(toPositionVS, normalVS); + const vec4 positionPrimeSS4 = u_Camera.ProjectionMatrix * vec4(positionVS.xyz + reflectVS, 1.0f); + vec3 positionPrimeSS = (positionPrimeSS4.xyz / positionPrimeSS4.w); + positionPrimeSS.xy = positionPrimeSS.xy * 0.5f + 0.5f; + const vec3 positionSS = vec3(uv, depth); + const vec3 reflectSS = vec3(positionPrimeSS - positionSS); + + uint iterations; + bool validHit = false; + vec3 ray = HierarchicalRaymarch(positionSS, reflectSS, baseDepthResolution, BASE_LOD, 4, u_SSRInfo.MaxSteps, iterations, validHit); + + vec4 totalColor; + if (u_SSRInfo.EnableConeTracing) + totalColor = ConeTracing(metalnessRoughness.g, positionSS.xy, ray.xy); + else + totalColor = texelFetch(u_InputColor, ivec2(ray.xy * textureSize(u_InputColor, 0)), 0); + + vec3 F0 = mix(vec3(0.04), albedo.rgb, metalnessRoughness.r); + + totalColor.rgb *= FresnelSchlickRoughness(F0, max(dot(normalVS, toPositionVS), 0.0), metalnessRoughness.g); + + float confidence = validHit ? ValidateHit(positionSS, ray, toPositionVS, reflectVS, metalnessRoughness.g) : 0; + + // From CesiumJS https://github.com/CesiumGS/cesium + totalColor *= max(1.f, dot(totalColor.rgb, vec3(0.2125, 0.7154, 0.0721) * u_SSRInfo.LuminanceFactor)) * u_SSRInfo.Brightness; + + // Reflection occlusion + float ao = 1.0f; + +#if GTAO_REFLECTION_OCCLUSION + #if __HZ_GTAO_COMPUTE_BENT_NORMALS + ao = (texture(u_GTAOTex, positionSS.xy).x >> 24) / 255.f; + #else + ao = texture(u_GTAOTex, positionSS.xy).x / 255.f; + #endif + ao = min(ao * XE_GTAO_OCCLUSION_TERM_SCALE, 1.0f); +#endif + +#if HBAO_REFLECTION_OCCLUSION + ao *= texture(u_HBAOTex, positionSS.xy).x; +#endif + float roughnessFactor = mix(clamp(0.0, 1.0, metalnessRoughness.r + ao), 0.0625f, pow(metalnessRoughness.g, 4)); + + // Confidence can be negtive. If it is, it will leave unwanted reflections. + imageStore(outColor, ivec2(base), vec4(roughnessFactor * totalColor.rgb, max(confidence, 0.0))); +} + + diff --git a/StarEditor/Resources/Shaders/PostProcessing/SceneComposite.glsl b/StarEditor/Resources/Shaders/PostProcessing/SceneComposite.glsl new file mode 100644 index 00000000..48d00141 --- /dev/null +++ b/StarEditor/Resources/Shaders/PostProcessing/SceneComposite.glsl @@ -0,0 +1,213 @@ +#version 450 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) out OutputBlock Output; + +void main() +{ + vec4 position = vec4(a_Position.xy, 0.0, 1.0); + Output.TexCoord = a_TexCoord; + gl_Position = position; +} + +#version 450 core +#pragma stage : frag + +#define HZ_RENDERER_EDGE_OUTLINE_EFFECT 0 +#define HZ_RENDERER_FOG_EFFECT 0 + +#include + +layout(location = 0) out vec4 o_Color; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) in OutputBlock Input; + +layout (set = 1, binding = 0) uniform sampler2D u_Texture; +layout (set = 1, binding = 1) uniform sampler2D u_BloomTexture; +layout (set = 1, binding = 2) uniform sampler2D u_BloomDirtTexture; +layout (set = 1, binding = 3) uniform sampler2D u_DepthTexture; +layout (set = 1, binding = 4) uniform sampler2D u_TransparentDepthTexture; + +#if HZ_RENDERER_EDGE_OUTLINE_EFFECT +layout (set = 1, binding = 5) uniform sampler2D u_EdgeTexture; +#endif + +layout(push_constant) uniform Uniforms +{ + float Exposure; + float BloomIntensity; + float BloomDirtIntensity; + float Opacity; + float Time; +} u_Uniforms; + +float LinearizeDepth(const float screenDepth) +{ + float depthLinearizeMul = u_Camera.DepthUnpackConsts.x; + float depthLinearizeAdd = u_Camera.DepthUnpackConsts.y; + return depthLinearizeMul / (depthLinearizeAdd - screenDepth); +} + +vec4 LinearizeDepth(vec4 deviceZs) +{ + return vec4(LinearizeDepth(deviceZs.x), LinearizeDepth(deviceZs.y), LinearizeDepth(deviceZs.z), LinearizeDepth(deviceZs.w)); +} + +vec3 UpsampleTent9(sampler2D tex, float lod, vec2 uv, vec2 texelSize, float radius) +{ + vec4 offset = texelSize.xyxy * vec4(1.0f, 1.0f, -1.0f, 0.0f) * radius; + + // Center + vec3 result = textureLod(tex, uv, lod).rgb * 4.0f; + + result += textureLod(tex, uv - offset.xy, lod).rgb; + result += textureLod(tex, uv - offset.wy, lod).rgb * 2.0; + result += textureLod(tex, uv - offset.zy, lod).rgb; + + result += textureLod(tex, uv + offset.zw, lod).rgb * 2.0; + result += textureLod(tex, uv + offset.xw, lod).rgb * 2.0; + + result += textureLod(tex, uv + offset.zy, lod).rgb; + result += textureLod(tex, uv + offset.wy, lod).rgb * 2.0; + result += textureLod(tex, uv + offset.xy, lod).rgb; + + return result * (1.0f / 16.0f); +} + +// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces +vec3 ACESTonemap(vec3 color) +{ + mat3 m1 = mat3( + 0.59719, 0.07600, 0.02840, + 0.35458, 0.90834, 0.13383, + 0.04823, 0.01566, 0.83777 + ); + mat3 m2 = mat3( + 1.60475, -0.10208, -0.00327, + -0.53108, 1.10813, -0.07276, + -0.07367, -0.00605, 1.07602 + ); + vec3 v = m1 * color; + vec3 a = v * (v + 0.0245786) - 0.000090537; + vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081; + return clamp(m2 * (a / b), 0.0, 1.0); +} + +vec3 GammaCorrect(vec3 color, float gamma) +{ + return pow(color, vec3(1.0f / gamma)); +} + +void main() +{ + const float gamma = 2.2; + const float pureWhite = 1.0; + float sampleScale = 0.5; + + vec3 color = texture(u_Texture, Input.TexCoord).rgb; + +#if HZ_RENDERER_EDGE_OUTLINE_EFFECT + { + const vec4 edgeColor = vec4(0.2, 0.2, 0.15, 1.0); + const vec4 backgroundColor = vec4(1,0.95,0.85,1); + float errorPeriod = 30.0; + float errorRange = 0.001; + + vec2 uvs[3]; + uvs[0] = Input.TexCoord + vec2(errorRange * sin(errorPeriod * Input.TexCoord.y + 0.0), errorRange * sin(errorPeriod * Input.TexCoord.x + 0.0)); + uvs[1] = Input.TexCoord + vec2(errorRange * sin(errorPeriod * Input.TexCoord.y + 1.047), errorRange * sin(errorPeriod * Input.TexCoord.x + 3.142)); + uvs[2] = Input.TexCoord + vec2(errorRange * sin(errorPeriod * Input.TexCoord.y + 2.094), errorRange * sin(errorPeriod * Input.TexCoord.x + 1.571)); + + float edge = texture(u_EdgeTexture, uvs[0]).r * texture(u_EdgeTexture, uvs[1]).r * texture(u_EdgeTexture, uvs[2]).r; + + // float edge = texture(u_EdgeTexture, Input.TexCoord).r; + // color *= edge; + + if (edge < 0.5) + { + color = mix(color, edgeColor.rgb * 0.1, 0.9); + } + + float diffuse = (color.x + color.y + color.z) / 3.0; + + float w = fwidth(diffuse) * 2.0; + vec4 mCol = mix(backgroundColor * 0.5, backgroundColor, mix(0.0, 1.0, smoothstep(-w, w, diffuse - 0.3))); + vec3 newCol = mix(edgeColor, mCol, edge).xyz; + //color = mix(newCol, color, 0.0); + edge += 0.5; + edge = clamp(edge, 0.0, 1.0); + } +#endif + + ivec2 texSize = textureSize(u_BloomTexture, 0); + vec2 fTexSize = vec2(float(texSize.x), float(texSize.y)); + vec3 bloom = UpsampleTent9(u_BloomTexture, 0, Input.TexCoord, 1.0f / fTexSize, sampleScale) * u_Uniforms.BloomIntensity; + vec3 bloomDirt = texture(u_BloomDirtTexture, Input.TexCoord).rgb * u_Uniforms.BloomDirtIntensity; + + // Fog +#if HZ_RENDERER_FOG_EFFECT + { + float depth = texture(u_DepthTexture, Input.TexCoord).r; + depth = LinearizeDepth(depth); + + float fogStartDistance = 5.5f; + //fogStartDistance = 1.0f; + float bloomFogStartDistance = 50.0f; + float fogFallOffDistance = 30.0f; + float bloomFogFallOffDistance = 50.0f; + + float fogAmount = smoothstep(fogStartDistance, fogStartDistance + fogFallOffDistance, depth); + float fogAmountBloom = smoothstep(bloomFogStartDistance, bloomFogStartDistance + bloomFogFallOffDistance, depth); + + // Skybox + //if (d == 0.0) + //{ + // color *= 0.2f; + // fogAmount = 0.0; + // fogAmountBloom = 0.0; + //} + + vec3 fogColor = vec3(0.11f, 0.12f, 0.15f); + fogColor *= 2.0f; + vec3 bloomClamped = clamp(bloom * (1.0f - fogAmountBloom), 0.0f, 1.0f); + float intensity = (bloomClamped.r + bloomClamped.g + bloomClamped.b) / 3.0f; + fogColor = mix(fogColor, color, intensity); + + color = mix(color, fogColor, fogAmount); + fogAmountBloom = clamp(fogAmountBloom, 0, 1); + //bloom *= (1.0f - fogAmountBloom); + } +#endif + + color += bloom; + color += bloom * bloomDirt; + color *= u_Uniforms.Exposure; + + // Grain + float strength = 5.0; + + float x = (Input.TexCoord.x + 1.0 ) * (Input.TexCoord.y + 1.0 ) * u_Uniforms.Time; + float grain = mod((mod(x, 13.0) + 1.0) * (mod(x, 123.0) + 1.0), 0.01) - 0.006; + + //color += grain * strength; + color = ACESTonemap(color); + + color = GammaCorrect(color.rgb, gamma); + + color *= u_Uniforms.Opacity; + + o_Color = vec4(color, 1.0); +} diff --git a/StarEditor/Resources/Shaders/Pre-Integration.glsl b/StarEditor/Resources/Shaders/Pre-Integration.glsl new file mode 100644 index 00000000..1846089d --- /dev/null +++ b/StarEditor/Resources/Shaders/Pre-Integration.glsl @@ -0,0 +1,49 @@ +#version 430 core +#pragma stage : comp + +layout(push_constant) uniform Info +{ + vec2 u_HZBInvRes; + vec2 u_InvRes; + vec2 u_ProjectionParams; //(x) = Near plane, (y) = Far plane // Reversed + int u_PrevMip; +}; + +layout(binding = 0, r8) restrict uniform writeonly image2D o_VisibilityImage; +layout(binding = 1) uniform sampler2D u_VisibilityTex; +layout(binding = 2) uniform sampler2D u_HZB; + +float LinearizeDepth(float d) +{ + return u_ProjectionParams.x * u_ProjectionParams.y / (u_ProjectionParams.y + d * (u_ProjectionParams.x - u_ProjectionParams.y)); +} + +layout(local_size_x = 8, local_size_y = 8) in; +void main() +{ + ivec2 base = ivec2(gl_GlobalInvocationID); + vec2 hzbUV = u_HZBInvRes * base; + vec2 uv = u_InvRes * base; + + vec4 fineZ; + fineZ.x = LinearizeDepth(textureLod(u_HZB, hzbUV + u_HZBInvRes * vec2(-0.5, -0.5), u_PrevMip).x); + fineZ.y = LinearizeDepth(textureLod(u_HZB, hzbUV + u_HZBInvRes * vec2(-0.5, 0.0), u_PrevMip).x); + fineZ.z = LinearizeDepth(textureLod(u_HZB, hzbUV + u_HZBInvRes * vec2( 0.0, -0.5), u_PrevMip).x); + fineZ.w = LinearizeDepth(textureLod(u_HZB, hzbUV + u_HZBInvRes * vec2( 0.5, 0.5), u_PrevMip).x); + + /* Fetch fine visibility from previous visibility map LOD */ + vec4 visibility; + visibility.x = textureLod(u_VisibilityTex, uv + u_InvRes * vec2(-0.5, -0.5), u_PrevMip).r; + visibility.y = textureLod(u_VisibilityTex, uv + u_InvRes * vec2(-0.5, 0.0), u_PrevMip).r; + visibility.z = textureLod(u_VisibilityTex, uv + u_InvRes * vec2( 0.0, -0.5), u_PrevMip).r; + visibility.w = textureLod(u_VisibilityTex, uv + u_InvRes * vec2( 0.5, 0.5), u_PrevMip).r; + + /* Integrate visibility */ + float maxZ = max(max(fineZ.x, fineZ.y), max(fineZ.z, fineZ.w)); + vec4 integration = (fineZ / maxZ) * visibility; + + /* Compute coarse visibility (with SIMD 'dot' intrinsic) */ + float coarseVisibility = dot(vec4(0.25), integration); + + imageStore(o_VisibilityImage, base, coarseVisibility.xxxx); +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/PreDepth.glsl b/StarEditor/Resources/Shaders/PreDepth.glsl new file mode 100644 index 00000000..e43acc37 --- /dev/null +++ b/StarEditor/Resources/Shaders/PreDepth.glsl @@ -0,0 +1,43 @@ +// Pre-depth shader + +#version 450 core +#pragma stage : vert + +#include + +// Vertex buffer +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +// Make sure both shaders compute the exact same answer(PBR shader). +// We need to have the same exact calculations to produce the gl_Position value (eg. matrix multiplications). +precise invariant gl_Position; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + vec4 worldPosition = transform * vec4(a_Position, 1.0); + + gl_Position = u_Camera.ViewProjectionMatrix * worldPosition; +} + +#version 450 core +#pragma stage : frag + +void main() +{ +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/PreDepth_Anim.glsl b/StarEditor/Resources/Shaders/PreDepth_Anim.glsl new file mode 100644 index 00000000..604e25a6 --- /dev/null +++ b/StarEditor/Resources/Shaders/PreDepth_Anim.glsl @@ -0,0 +1,72 @@ +// Pre-depth shader + +#version 450 core +#pragma stage : vert + +#include + +// Vertex buffer +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +// Bone influences +layout(location = 8) in ivec4 a_BoneIndices; +layout(location = 9) in vec4 a_BoneWeights; + +layout(push_constant) uniform BoneTransformIndex +{ + uint Base; + uint Stride; +} u_BoneTransformIndex; + +// layout(location = 0) out float LinearDepth; + +// Make sure both shaders compute the exact same answer(PBR shader). +// We need to have the same exact calculations to produce the gl_Position value (eg. matrix multiplications). +precise invariant gl_Position; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + mat4 boneTransform = r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[0]] * a_BoneWeights[0]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[1]] * a_BoneWeights[1]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[2]] * a_BoneWeights[2]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[3]] * a_BoneWeights[3]; + + vec4 worldPosition = transform * boneTransform * vec4(a_Position, 1.0); + + //vec4 viewPosition = u_ViewMatrix * worldPosition; + // Linear depth is not flipped. + //LinearDepth = -viewPosition.z; + + // Near and far are flipped for better precision. + // Only change along with the PBR shader. + gl_Position = u_Camera.ViewProjectionMatrix * worldPosition; +} + +#version 450 core +#pragma stage : frag + +//layout(location = 0) out vec4 o_LinearDepth; + +//layout(location = 0) in float LinearDepth; + +void main() +{ + // TODO: Check for alpha in texture + //o_LinearDepth = vec4(LinearDepth, 0.0, 0.0, 1.0); +} diff --git a/StarEditor/Resources/Shaders/PreethamSky.glsl b/StarEditor/Resources/Shaders/PreethamSky.glsl new file mode 100644 index 00000000..2cc15da6 --- /dev/null +++ b/StarEditor/Resources/Shaders/PreethamSky.glsl @@ -0,0 +1,143 @@ +// +// Preetham Sky shader +// Heavily adapted from https://www.shadertoy.com/view/llSSDR +// + +#version 450 core +#pragma stage : comp + +const float PI = 3.141592; + +layout(binding = 0, rgba32f) restrict writeonly uniform imageCube o_CubeMap; + +layout (push_constant) uniform Uniforms +{ + vec3 TurbidityAzimuthInclination; +} u_Uniforms; + +vec3 GetCubeMapTexCoord() +{ + vec2 st = gl_GlobalInvocationID.xy / vec2(imageSize(o_CubeMap)); + vec2 uv = 2.0 * vec2(st.x, 1.0 - st.y) - vec2(1.0); + + vec3 ret; + if (gl_GlobalInvocationID.z == 0) ret = vec3( 1.0, uv.y, -uv.x); + else if (gl_GlobalInvocationID.z == 1) ret = vec3( -1.0, uv.y, uv.x); + else if (gl_GlobalInvocationID.z == 2) ret = vec3( uv.x, 1.0, -uv.y); + else if (gl_GlobalInvocationID.z == 3) ret = vec3( uv.x, -1.0, uv.y); + else if (gl_GlobalInvocationID.z == 4) ret = vec3( uv.x, uv.y, 1.0); + else if (gl_GlobalInvocationID.z == 5) ret = vec3(-uv.x, uv.y, -1.0); + return normalize(ret); +} + +#define PI 3.14159265359 + +float saturatedDot( in vec3 a, in vec3 b ) +{ + return max( dot( a, b ), 0.0 ); +} + +vec3 YxyToXYZ( in vec3 Yxy ) +{ + float Y = Yxy.r; + float x = Yxy.g; + float y = Yxy.b; + + float X = x * ( Y / y ); + float Z = ( 1.0 - x - y ) * ( Y / y ); + + return vec3(X,Y,Z); +} + +vec3 XYZToRGB( in vec3 XYZ ) +{ + // CIE/E + mat3 M = mat3 + ( + 2.3706743, -0.9000405, -0.4706338, + -0.5138850, 1.4253036, 0.0885814, + 0.0052982, -0.0146949, 1.0093968 + ); + + return XYZ * M; +} + + +vec3 YxyToRGB( in vec3 Yxy ) +{ + vec3 XYZ = YxyToXYZ( Yxy ); + vec3 RGB = XYZToRGB( XYZ ); + return RGB; +} + +void calculatePerezDistribution( in float t, out vec3 A, out vec3 B, out vec3 C, out vec3 D, out vec3 E ) +{ + A = vec3( 0.1787 * t - 1.4630, -0.0193 * t - 0.2592, -0.0167 * t - 0.2608 ); + B = vec3( -0.3554 * t + 0.4275, -0.0665 * t + 0.0008, -0.0950 * t + 0.0092 ); + C = vec3( -0.0227 * t + 5.3251, -0.0004 * t + 0.2125, -0.0079 * t + 0.2102 ); + D = vec3( 0.1206 * t - 2.5771, -0.0641 * t - 0.8989, -0.0441 * t - 1.6537 ); + E = vec3( -0.0670 * t + 0.3703, -0.0033 * t + 0.0452, -0.0109 * t + 0.0529 ); +} + +vec3 calculateZenithLuminanceYxy( in float t, in float thetaS ) +{ + float chi = ( 4.0 / 9.0 - t / 120.0 ) * ( PI - 2.0 * thetaS ); + float Yz = ( 4.0453 * t - 4.9710 ) * tan( chi ) - 0.2155 * t + 2.4192; + + float theta2 = thetaS * thetaS; + float theta3 = theta2 * thetaS; + float T = t; + float T2 = t * t; + + float xz = + ( 0.00165 * theta3 - 0.00375 * theta2 + 0.00209 * thetaS + 0.0) * T2 + + (-0.02903 * theta3 + 0.06377 * theta2 - 0.03202 * thetaS + 0.00394) * T + + ( 0.11693 * theta3 - 0.21196 * theta2 + 0.06052 * thetaS + 0.25886); + + float yz = + ( 0.00275 * theta3 - 0.00610 * theta2 + 0.00317 * thetaS + 0.0) * T2 + + (-0.04214 * theta3 + 0.08970 * theta2 - 0.04153 * thetaS + 0.00516) * T + + ( 0.15346 * theta3 - 0.26756 * theta2 + 0.06670 * thetaS + 0.26688); + + return vec3( Yz, xz, yz ); +} + +vec3 calculatePerezLuminanceYxy( in float theta, in float gamma, in vec3 A, in vec3 B, in vec3 C, in vec3 D, in vec3 E ) +{ + return ( 1.0 + A * exp( B / cos( theta ) ) ) * ( 1.0 + C * exp( D * gamma ) + E * cos( gamma ) * cos( gamma ) ); +} + +vec3 calculateSkyLuminanceRGB( in vec3 s, in vec3 e, in float t ) +{ + vec3 A, B, C, D, E; + calculatePerezDistribution( t, A, B, C, D, E ); + + float thetaS = acos( saturatedDot( s, vec3(0,1,0) ) ); + float thetaE = acos( saturatedDot( e, vec3(0,1,0) ) ); + float gammaE = acos( saturatedDot( s, e ) ); + + vec3 Yz = calculateZenithLuminanceYxy( t, thetaS ); + + vec3 fThetaGamma = calculatePerezLuminanceYxy( thetaE, gammaE, A, B, C, D, E ); + vec3 fZeroThetaS = calculatePerezLuminanceYxy( 0.0, thetaS, A, B, C, D, E ); + + vec3 Yp = Yz * ( fThetaGamma / fZeroThetaS ); + + return YxyToRGB( Yp ); +} + +layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in; +void main() +{ + vec3 cubeTC = GetCubeMapTexCoord(); + + float turbidity = u_Uniforms.TurbidityAzimuthInclination.x; + float azimuth = u_Uniforms.TurbidityAzimuthInclination.y;; + float inclination = u_Uniforms.TurbidityAzimuthInclination.z; + vec3 sunDir = normalize( vec3( sin(inclination) * cos(azimuth), cos(inclination), sin(inclination) * sin(azimuth) ) ); + vec3 viewDir = cubeTC; + vec3 skyLuminance = calculateSkyLuminanceRGB( sunDir, viewDir, turbidity ); + + vec4 color = vec4(skyLuminance * 0.05, 1.0); + imageStore(o_CubeMap, ivec3(gl_GlobalInvocationID), color); +} diff --git a/StarEditor/Resources/Shaders/Renderer2D.glsl b/StarEditor/Resources/Shaders/Renderer2D.glsl new file mode 100644 index 00000000..36393401 --- /dev/null +++ b/StarEditor/Resources/Shaders/Renderer2D.glsl @@ -0,0 +1,65 @@ +// Basic Texture Shader + +#version 450 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec4 a_Color; +layout(location = 2) in vec2 a_TexCoord; +layout(location = 3) in float a_TexIndex; +layout(location = 4) in float a_TilingFactor; + +layout (std140, set = 1, binding = 0) uniform Camera +{ + mat4 u_ViewProjection; +}; + +layout (push_constant) uniform Transform +{ + mat4 Transform; +} u_Renderer; + +struct VertexOutput +{ + vec4 Color; + vec2 TexCoord; + float TilingFactor; +}; + +layout (location = 0) out VertexOutput Output; +layout (location = 5) out flat float TexIndex; + +void main() +{ + Output.Color = a_Color; + Output.TexCoord = a_TexCoord; + TexIndex = a_TexIndex; + Output.TilingFactor = a_TilingFactor; + gl_Position = u_ViewProjection * u_Renderer.Transform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 color; + +struct VertexOutput +{ + vec4 Color; + vec2 TexCoord; + float TilingFactor; +}; + +layout (location = 0) in VertexOutput Input; +layout (location = 5) in flat float TexIndex; + +layout (set = 0, binding = 0) uniform sampler2D u_Textures[32]; + +void main() +{ + color = texture(u_Textures[int(TexIndex)], Input.TexCoord * Input.TilingFactor) * Input.Color; + + // Discard to avoid depth write + if (color.a == 0.0) + discard; +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/Renderer2D_Circle.glsl b/StarEditor/Resources/Shaders/Renderer2D_Circle.glsl new file mode 100644 index 00000000..37a9162a --- /dev/null +++ b/StarEditor/Resources/Shaders/Renderer2D_Circle.glsl @@ -0,0 +1,63 @@ +// Basic Texture Shader + +#version 430 core +#pragma stage : vert + +layout(location = 0) in vec3 a_WorldPosition; +layout(location = 1) in float a_Thickness; +layout(location = 2) in vec2 a_LocalPosition; +layout(location = 3) in vec4 a_Color; + +layout (std140, set = 1, binding = 0) uniform Camera +{ + mat4 u_ViewProjection; +}; + +layout (push_constant) uniform Transform +{ + mat4 Transform; +} u_Renderer; + +struct VertexOutput +{ + vec2 LocalPosition; + float Thickness; + vec4 Color; +}; + +layout (location = 0) out VertexOutput Output; + +void main() +{ + Output.LocalPosition = a_LocalPosition; + Output.Thickness = a_Thickness; + Output.Color = a_Color; + gl_Position = u_ViewProjection * u_Renderer.Transform * vec4(a_WorldPosition, 1.0); +} + +#version 430 core +#pragma stage : frag + +layout(location = 0) out vec4 color; + +struct VertexOutput +{ + vec2 LocalPosition; + float Thickness; + vec4 Color; +}; + +layout (location = 0) in VertexOutput Input; + +void main() +{ + float fade = 0.01; + float dist = sqrt(dot(Input.LocalPosition, Input.LocalPosition)); + if (dist > 1.0 || dist < 1.0 - Input.Thickness - fade) + discard; + + float alpha = 1.0 - smoothstep(1.0f - fade, 1.0f, dist); + alpha *= smoothstep(1.0 - Input.Thickness - fade, 1.0 - Input.Thickness, dist); + color = Input.Color; + color.a = alpha; +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/Renderer2D_Line.glsl b/StarEditor/Resources/Shaders/Renderer2D_Line.glsl new file mode 100644 index 00000000..c56d960c --- /dev/null +++ b/StarEditor/Resources/Shaders/Renderer2D_Line.glsl @@ -0,0 +1,37 @@ +// Basic Texture Shader + +#version 450 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec4 a_Color; + +layout (std140, set = 1, binding = 0) uniform Camera +{ + mat4 u_ViewProjection; +}; + +layout (push_constant) uniform Transform +{ + mat4 Transform; +} u_Renderer; + +layout (location = 0) out vec4 v_Color; + +void main() +{ + v_Color = a_Color; + gl_Position = u_ViewProjection * u_Renderer.Transform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 color; + +layout (location = 0) in vec4 v_Color; + +void main() +{ + color = v_Color; +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/Renderer2D_Text.glsl b/StarEditor/Resources/Shaders/Renderer2D_Text.glsl new file mode 100644 index 00000000..2fd4c030 --- /dev/null +++ b/StarEditor/Resources/Shaders/Renderer2D_Text.glsl @@ -0,0 +1,88 @@ +// Basic Texture Shader + +#version 450 core +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec4 a_Color; +layout(location = 2) in vec2 a_TexCoord; +layout(location = 3) in float a_TexIndex; + +layout (std140, set = 1, binding = 0) uniform Camera +{ + mat4 u_ViewProjection; +}; + +layout (push_constant) uniform Transform +{ + mat4 Transform; +} u_Renderer; + +struct VertexOutput +{ + vec4 Color; + vec2 TexCoord; +}; + +layout (location = 0) out VertexOutput Output; +layout (location = 5) out flat float TexIndex; + +void main() +{ + Output.Color = a_Color; + Output.TexCoord = a_TexCoord; + TexIndex = a_TexIndex; + gl_Position = u_ViewProjection * u_Renderer.Transform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 color; + +struct VertexOutput +{ + vec4 Color; + vec2 TexCoord; +}; + +layout (location = 0) in VertexOutput Input; +layout (location = 5) in flat float TexIndex; + +layout (set = 0, binding = 0) uniform sampler2D u_FontAtlases[32]; + +float median(float r, float g, float b) +{ + return max(min(r, g), min(max(r, g), b)); +} + +/* For 2D +float ScreenPxRange() +{ + float pixRange = 2.0f; + float geoSize = 72.0f; + return geoSize / 32.0f * pixRange; +} +*/ + +float ScreenPxRange() +{ + float pxRange = 2.0f; + vec2 unitRange = vec2(pxRange)/vec2(textureSize(u_FontAtlases[int(TexIndex)], 0)); + vec2 screenTexSize = vec2(1.0)/fwidth(Input.TexCoord); + return max(0.5*dot(unitRange, screenTexSize), 1.0); +} + +void main() +{ + vec4 bgColor = vec4(Input.Color.rgb, 0.0); // TODO(Yan): outlines + vec4 fgColor = Input.Color; + + // NOTE(Yan): MSDF texture has no mips (only LOD 0), but in the future it might + // be nice to do some sort of fading/smoothing when camera is far + vec3 msd = texture(u_FontAtlases[int(TexIndex)], Input.TexCoord).rgb; + float sd = median(msd.r, msd.g, msd.b); + float screenPxDistance = ScreenPxRange() * (sd - 0.5f); + float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); + color = mix(bgColor, fgColor, opacity); +} \ No newline at end of file diff --git a/StarEditor/Resources/Shaders/SelectedGeometry.glsl b/StarEditor/Resources/Shaders/SelectedGeometry.glsl new file mode 100644 index 00000000..e658949c --- /dev/null +++ b/StarEditor/Resources/Shaders/SelectedGeometry.glsl @@ -0,0 +1,38 @@ +#version 450 core +#pragma stage : vert + +#include + +// Vertex buffer +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + gl_Position = u_Camera.ViewProjectionMatrix * transform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 o_Color; + +void main() +{ + o_Color = vec4(1.0f); +} diff --git a/StarEditor/Resources/Shaders/SelectedGeometry_Anim.glsl b/StarEditor/Resources/Shaders/SelectedGeometry_Anim.glsl new file mode 100644 index 00000000..2c913971 --- /dev/null +++ b/StarEditor/Resources/Shaders/SelectedGeometry_Anim.glsl @@ -0,0 +1,53 @@ +#version 450 core +#pragma stage : vert + +#include + +// Vertex buffer +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +// Bone influences +layout(location = 8) in ivec4 a_BoneIndices; +layout(location = 9) in vec4 a_BoneWeights; + +layout(push_constant) uniform BoneTransformIndex +{ + uint Base; + uint Stride; +} u_BoneTransformIndex; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + mat4 boneTransform = r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[0]] * a_BoneWeights[0]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[1]] * a_BoneWeights[1]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[2]] * a_BoneWeights[2]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[3]] * a_BoneWeights[3]; + + gl_Position = u_Camera.ViewProjectionMatrix * transform * boneTransform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 o_Color; + +void main() +{ + o_Color = vec4(1.0f); +} diff --git a/StarEditor/Resources/Shaders/Skybox.glsl b/StarEditor/Resources/Shaders/Skybox.glsl new file mode 100644 index 00000000..e96e45d8 --- /dev/null +++ b/StarEditor/Resources/Shaders/Skybox.glsl @@ -0,0 +1,39 @@ +// Skybox shader + +#version 450 core +#pragma stage : vert +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +layout(location = 0) out vec3 v_Position; + +void main() +{ + vec4 position = vec4(a_Position.xy, 0.0, 1.0); + gl_Position = position; + + v_Position = (u_Camera.InverseViewProjectionMatrix * position).xyz; +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 finalColor; + +layout (set = 0, binding = 1) uniform samplerCube u_Texture; + +layout (push_constant) uniform Uniforms +{ + float TextureLod; + float Intensity; +} u_Uniforms; + +layout (location = 0) in vec3 v_Position; + +void main() +{ + finalColor = textureLod(u_Texture, v_Position, u_Uniforms.TextureLod) * u_Uniforms.Intensity; + finalColor.a = 1.0f; +} diff --git a/StarEditor/Resources/Shaders/SpotShadowMap.glsl b/StarEditor/Resources/Shaders/SpotShadowMap.glsl new file mode 100644 index 00000000..0bff3d00 --- /dev/null +++ b/StarEditor/Resources/Shaders/SpotShadowMap.glsl @@ -0,0 +1,42 @@ +// Spot Shadow Map shader + +#version 450 core +#pragma stage : vert + +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +layout(push_constant) uniform Transform +{ + int LightIndex; +} u_Renderer; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + gl_Position = u_SpotLightMatrices.Mats[u_Renderer.LightIndex] * transform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +void main() +{ + // TODO: Check for alpha in texture +} diff --git a/StarEditor/Resources/Shaders/SpotShadowMap_Anim.glsl b/StarEditor/Resources/Shaders/SpotShadowMap_Anim.glsl new file mode 100644 index 00000000..3fe5c551 --- /dev/null +++ b/StarEditor/Resources/Shaders/SpotShadowMap_Anim.glsl @@ -0,0 +1,53 @@ +// Spot Shadow Map shader + +#version 450 core +#pragma stage : vert + +#include + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +// Bone influences +layout(location = 8) in ivec4 a_BoneIndices; +layout(location = 9) in vec4 a_BoneWeights; + +layout(push_constant) uniform PushConstants +{ + uint LightIndex; + uint BoneTransformBaseIndex; + uint BoneTransformStride; +} u_Constants; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + mat4 boneTransform = r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[0]] * a_BoneWeights[0]; + boneTransform += r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[1]] * a_BoneWeights[1]; + boneTransform += r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[2]] * a_BoneWeights[2]; + boneTransform += r_BoneTransforms.BoneTransforms[u_Constants.BoneTransformBaseIndex + (gl_InstanceIndex * u_Constants.BoneTransformStride) + a_BoneIndices[3]] * a_BoneWeights[3]; + + gl_Position = u_SpotLightMatrices.Mats[u_Constants.LightIndex] * transform * boneTransform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +void main() +{ + // TODO: Check for alpha in texture +} diff --git a/StarEditor/Resources/Shaders/TexturePass.glsl b/StarEditor/Resources/Shaders/TexturePass.glsl new file mode 100644 index 00000000..a78a90e5 --- /dev/null +++ b/StarEditor/Resources/Shaders/TexturePass.glsl @@ -0,0 +1,37 @@ +#version 450 core +#pragma stage : vert +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec2 a_TexCoord; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) out OutputBlock Output; + +void main() +{ + vec4 position = vec4(a_Position.xy, 0.0, 1.0); + Output.TexCoord = a_TexCoord; + gl_Position = position; +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 o_Color; + +struct OutputBlock +{ + vec2 TexCoord; +}; + +layout (location = 0) in OutputBlock Input; + +layout (binding = 0) uniform sampler2D u_Texture; + +void main() +{ + o_Color = texture(u_Texture, Input.TexCoord); +} diff --git a/StarEditor/Resources/Shaders/Wireframe.glsl b/StarEditor/Resources/Shaders/Wireframe.glsl new file mode 100644 index 00000000..a19afe64 --- /dev/null +++ b/StarEditor/Resources/Shaders/Wireframe.glsl @@ -0,0 +1,48 @@ +// Outline Shader + +#version 450 core +#pragma stage : vert +#include + +layout(location = 0) in vec3 a_Position; + +////////////////////////////////////////// +// UNUSED +////////////////////////////////////////// +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; +////////////////////////////////////////// + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + gl_Position = u_Camera.ViewProjectionMatrix * transform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 color; + +layout (push_constant) uniform Material +{ + vec4 Color; +} u_MaterialUniforms; + +void main() +{ + color = u_MaterialUniforms.Color; +} diff --git a/StarEditor/Resources/Shaders/Wireframe_Anim.glsl b/StarEditor/Resources/Shaders/Wireframe_Anim.glsl new file mode 100644 index 00000000..a28376e8 --- /dev/null +++ b/StarEditor/Resources/Shaders/Wireframe_Anim.glsl @@ -0,0 +1,64 @@ +// Outline Shader + +#version 450 core +#pragma stage : vert +#include + +layout(location = 0) in vec3 a_Position; + +////////////////////////////////////////// +// UNUSED +////////////////////////////////////////// +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Binormal; +layout(location = 4) in vec2 a_TexCoord; +////////////////////////////////////////// + + +// Transform buffer +layout(location = 5) in vec4 a_MRow0; +layout(location = 6) in vec4 a_MRow1; +layout(location = 7) in vec4 a_MRow2; + +// Bone influences +layout(location = 8) in ivec4 a_BoneIndices; +layout(location = 9) in vec4 a_BoneWeights; + +layout(push_constant) uniform BoneTransformIndex +{ + uint Base; + uint Stride; +} u_BoneTransformIndex; + +void main() +{ + mat4 transform = mat4( + vec4(a_MRow0.x, a_MRow1.x, a_MRow2.x, 0.0), + vec4(a_MRow0.y, a_MRow1.y, a_MRow2.y, 0.0), + vec4(a_MRow0.z, a_MRow1.z, a_MRow2.z, 0.0), + vec4(a_MRow0.w, a_MRow1.w, a_MRow2.w, 1.0) + ); + + mat4 boneTransform = r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[0]] * a_BoneWeights[0]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[1]] * a_BoneWeights[1]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[2]] * a_BoneWeights[2]; + boneTransform += r_BoneTransforms.BoneTransforms[u_BoneTransformIndex.Base + (gl_InstanceIndex * u_BoneTransformIndex.Stride) + a_BoneIndices[3]] * a_BoneWeights[3]; + + gl_Position = u_Camera.ViewProjectionMatrix * transform * boneTransform * vec4(a_Position, 1.0); +} + +#version 450 core +#pragma stage : frag + +layout(location = 0) out vec4 color; + +layout (push_constant) uniform Material +{ + layout (offset = 16) vec4 Color; +} u_MaterialUniforms; + +void main() +{ + color = u_MaterialUniforms.Color; +} diff --git a/StarEditor/Resources/Shaders/shader.glsl b/StarEditor/Resources/Shaders/shader.glsl new file mode 100644 index 00000000..bec93c2e --- /dev/null +++ b/StarEditor/Resources/Shaders/shader.glsl @@ -0,0 +1,35 @@ +#version 430 +#pragma stage : vert + +layout(location = 0) in vec3 a_Position; +layout(location = 1) in vec3 a_Normal; +layout(location = 2) in vec3 a_Tangent; +layout(location = 3) in vec3 a_Bitangent; +layout(location = 4) in vec2 a_TexCoord; + +uniform mat4 u_MVP; + +out vec3 v_Normal; + +void main() +{ + gl_Position = u_MVP * vec4(a_Position, 1.0); + v_Normal = a_Normal; +} + +#version 430 +#pragma stage : frag + +layout(location = 0) out vec4 finalColor; + +uniform vec4 u_Color; + +in vec3 v_Normal; + +void main() +{ + finalColor = vec4(0.8, 0.0, 0.8, 1.0); + + + finalColor = vec4((v_Normal * 0.5 + 0.5), 1.0);// * u_Color.xyz, 1.0); +} \ No newline at end of file diff --git a/StarEditor/Resources/Templates/NewClassTemplate.cs b/StarEditor/Resources/Templates/NewClassTemplate.cs new file mode 100644 index 00000000..62cc6ddb --- /dev/null +++ b/StarEditor/Resources/Templates/NewClassTemplate.cs @@ -0,0 +1,24 @@ +using System; +using Hazel; + +namespace $NAMESPACE_NAME$ +{ + public class $CLASS_NAME$ : Entity + { + /// + /// OnCreate is called once when the Entity that this script is attached to + /// is instantiated in the scene at runtime + /// + protected override void OnCreate() + { + } + + /// + /// OnUpdate is called once every frame while this script is active in the scene + /// + protected override void OnUpdate(float deltaTime) + { + } + + } +} diff --git a/StarEditor/premake5.lua b/StarEditor/premake5.lua index 79800d69..c6c1aebf 100644 --- a/StarEditor/premake5.lua +++ b/StarEditor/premake5.lua @@ -1,57 +1,91 @@ project "StarEditor" - kind "ConsoleApp" - language "C++" - cppdialect "C++17" - staticruntime "off" + kind "ConsoleApp" + + debuggertype "NativeWithManagedCore" targetdir ("%{wks.location}/bin/" .. outputdir .. "/%{prj.name}") objdir ("%{wks.location}/bin-int/" .. outputdir .. "/%{prj.name}") - files - { + links { "StarEngine" } + + defines { "GLM_FORCE_DEPTH_ZERO_TO_ONE", } + + files { "src/**.h", - "src/**.cpp" - } + "src/**.c", + "src/**.hpp", + "src/**.cpp", - includedirs - { - "%{wks.location}/StarEngine/vendor/spdlog/include", - "%{wks.location}/StarEngine/src", - "%{wks.location}/StarEngine/vendor", - "%{IncludeDir.filewatch}", - "%{IncludeDir.glm}", - "%{IncludeDir.entt}", - "%{IncludeDir.ImGuizmo}", - "%{IncludeDir.miniaudio}" + "../StarEngine/vendor/glm/glm/**.hpp", + + -- Shaders + "Resources/Shaders/**.glsl", + "Resources/Shaders/**.glslh", + "Resources/Shaders/**.hlsl", + "Resources/Shaders/**.hlslh", + "Resources/Shaders/**.slh", } - links - { - "StarEngine" + includedirs { + "src/", + + "../StarEngine/src/", + "../StarEngine/vendor/", + "../StarEngine/vendor/spdlog/include", + "../StarEngine/vendor/glm", } - filter "system:windows" + filter "system:windows" systemversion "latest" + + defines { "SE_PLATFORM_WINDOWS" } + + postbuildcommands { + --'{COPY} "../StarEngine/vendor/NvidiaAftermath/lib/x64/windows/GFSDK_Aftermath_Lib.x64.dll" "%{cfg.targetdir}"', + } + + filter { "system:windows", "configurations:Debug or configurations:Debug-AS" } postbuildcommands { - "{COPYDIR} %{wks.location}/StarEditor/assets %{wks.location}/bin/" .. outputdir .. "/StarEditor/assets", - "{COPYDIR} %{wks.location}/StarEditor/Resources %{wks.location}/bin/" .. outputdir .. "/StarEditor/Resources", - "{COPYFILE} %{wks.location}/StarEditor/imgui.ini %{wks.location}/bin/" .. outputdir .. "/StarEditor/imgui.ini", + --'{COPY} "../StarEngine/vendor/assimp/bin/windows/Debug/assimp-vc143-mtd.dll" "%{cfg.targetdir}"', } - filter "configurations:Debug" - defines "SE_DEBUG" - runtime "Debug" - symbols "on" + filter { "system:windows", "configurations:Release or configurations:Dist" } + postbuildcommands { + --'{COPY} "../StarEngine/vendor/assimp/bin/windows/Release/assimp-vc143-mt.dll" "%{cfg.targetdir}"', + } + + filter "system:linux" + defines { "SE_PLATFORM_LINUX", "__EMULATE_UUID", "BACKWARD_HAS_DW", "BACKWARD_HAS_LIBUNWIND" } + links { "dw", "dl", "unwind", "pthread" } + + result, err = os.outputof("pkg-config --libs gtk+-3.0") + linkoptions { result } + + filter "configurations:Debug or configurations:Debug-AS" + symbols "On" + defines { "SE_DEBUG" } + + filter { "system:windows", "configurations:Debug-AS" } + sanitize { "Address" } + flags { "NoRuntimeChecks", "NoIncrementalLink" } filter "configurations:Release" - defines "SE_RELEASE" - runtime "Release" - optimize "on" + optimize "On" + vectorextensions "AVX2" + isaextensions { "BMI", "POPCNT", "LZCNT", "F16C" } + defines { "SE_RELEASE", } + + filter "configurations:Debug or configurations:Debug-AS or configurations:Release" + defines { + "SE_TRACK_MEMORY", + + "JPH_DEBUG_RENDERER", + "JPH_FLOATING_POINT_EXCEPTIONS_ENABLED", + "JPH_EXTERNAL_PROFILE" + } - filter "configurations:Dist" - defines "SE_DIST" - runtime "Release" - optimize "on" + filter "files:**.hlsl" + flags {"ExcludeFromBuild"} - filter "action:vs2022" - buildoptions { "/utf-8" } + filter "configurations:Dist" + flags { "ExcludeFromBuild" } diff --git a/StarEditor/src/EditorLayer.cpp b/StarEditor/src/EditorLayer.cpp index c83ca63d..6e6ab156 100644 --- a/StarEditor/src/EditorLayer.cpp +++ b/StarEditor/src/EditorLayer.cpp @@ -1,10 +1,10 @@ +#include "sepch.h" #include "EditorLayer.h" #include "StarEngine/Scene/SceneSerializer.h" -#include "StarEngine/Utils/PlatformUtils.h" -#include "StarEngine/Math/Math.h" +#include "StarEngine/Utilities/PlatformUtils.h" #include "StarEngine/Scripting/ScriptEngine.h" -#include "StarEngine/Renderer/Font.h" +#include "StarEngine/Renderer/UI/Font.h" #include "StarEngine/Asset/AssetManager.h" #include "StarEngine/Asset/TextureImporter.h" @@ -31,7 +31,7 @@ namespace StarEngine { void EditorLayer::OnAttach() { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("EditorLayer::OnAttach"); m_IconPlay = TextureImporter::LoadTexture2D("Resources/Icons/PlayButton.png"); m_IconPause = TextureImporter::LoadTexture2D("Resources/Icons/PauseButton.png"); @@ -45,7 +45,7 @@ namespace StarEngine { fbSpec.Height = 720; m_Framebuffer = Framebuffer::Create(fbSpec); - m_EditorScene = CreateRef(); + m_EditorScene = Ref::Create(); m_ActiveScene = m_EditorScene; auto commandLineArgs = Application::Get().GetSpecification().CommandLineArgs; @@ -72,12 +72,12 @@ namespace StarEngine { void EditorLayer::OnDetach() { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("EditorLayer::OnDetach"); } void EditorLayer::OnUpdate(Timestep ts) { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("EditorLayer::OnUpdate"); m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y); @@ -93,7 +93,7 @@ namespace StarEngine { // Render Renderer2D::ResetStats(); - m_Framebuffer->Bind(); + // m_Framebuffer->Bind(); RenderCommand::SetClearColor({ 0.1f, 0.1f, 0.1f, 1 }); RenderCommand::Clear(); @@ -150,7 +150,7 @@ namespace StarEngine { void EditorLayer::OnImGuiRender() { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("EditorLayer::OnImGuiRender"); // Note: Switch this to true to enable dockspace static bool dockspaceOpen = true; @@ -391,7 +391,7 @@ namespace StarEngine { if (hasPlayButton) { - Ref icon = (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Simulate) ? m_IconPlay : m_IconStop; + RefPtr icon = (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Simulate) ? m_IconPlay : m_IconStop; if (ImGui::ImageButton("##play", (ImTextureID)(uint64_t)icon->GetRendererID(), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1))) { if (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Simulate) OnScenePlay(); @@ -405,7 +405,7 @@ namespace StarEngine { if (hasPlayButton) ImGui::SameLine(); - Ref icon = (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Play) ? m_IconSimulate : m_IconStop; //ImGui::SetCursorPosX((ImGui::GetWindowContentRegionMax().x * 0.5f) - (size * 0.5f)); + RefPtr icon = (m_SceneState == SceneState::Edit || m_SceneState == SceneState::Play) ? m_IconSimulate : m_IconStop; //ImGui::SetCursorPosX((ImGui::GetWindowContentRegionMax().x * 0.5f) - (size * 0.5f)); if (ImGui::ImageButton("##simulate", (ImTextureID)(uint64_t)icon->GetRendererID(), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1))) @@ -422,7 +422,7 @@ namespace StarEngine { bool isPaused = m_ActiveScene->IsPaused(); ImGui::SameLine(); { - Ref icon = m_IconPause; + RefPtr icon = m_IconPause; if (ImGui::ImageButton("##pause", (ImTextureID)(uint64_t)icon->GetRendererID(), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1)) && toolbarEnabled) { m_ActiveScene->SetPaused(!isPaused); @@ -435,7 +435,7 @@ namespace StarEngine { { ImGui::SameLine(); { - Ref icon = m_IconStep; + RefPtr icon = m_IconStep; bool isPaused = m_ActiveScene->IsPaused(); if (ImGui::ImageButton("##step", (ImTextureID)(uint64_t)icon->GetRendererID(), ImVec2(size, size), ImVec2(0, 0), ImVec2(1, 1)) && toolbarEnabled) @@ -667,7 +667,7 @@ namespace StarEngine { AssetHandle startScene = Project::GetActive()->GetConfig().StartScene; if (startScene) OpenScene(startScene); - m_ContentBrowserPanel = CreateScope(Project::GetActive()); + m_ContentBrowserPanel = std::make_unique(Project::GetActive()); } } @@ -688,7 +688,7 @@ namespace StarEngine { void EditorLayer::NewScene() { - m_ActiveScene = CreateRef(); + m_ActiveScene = Ref::Create(); m_ActiveScene->OnViewportResize((uint32_t)m_ViewportSize.x, (uint32_t)m_ViewportSize.y); m_SceneHierarchyPanel.SetContext(m_ActiveScene); @@ -697,7 +697,7 @@ namespace StarEngine { void EditorLayer::OpenScene() { - // std::string filepath = FileDialogs::OpenFile("Hazel Scene (*.hazel)\0*.hazel\0"); + // std::string filepath = FileDialogs::OpenFile("StarEngine Scene (*.hazel)\0*.hazel\0"); // if (!filepath.empty()) // OpenScene(filepath); } @@ -737,7 +737,7 @@ namespace StarEngine { } } - void EditorLayer::SerializeScene(Ref scene, const std::filesystem::path& path) + void EditorLayer::SerializeScene(RefPtr scene, const std::filesystem::path& path) { SceneImporter::SaveScene(scene, path); } diff --git a/StarEditor/src/EditorLayer.h b/StarEditor/src/EditorLayer.h index 7950abc0..9f76ad61 100644 --- a/StarEditor/src/EditorLayer.h +++ b/StarEditor/src/EditorLayer.h @@ -42,7 +42,7 @@ namespace StarEngine void SaveScene(); void SaveSceneAs(); - void SerializeScene(Ref scene, const std::filesystem::path& filepath); + void SerializeScene(RefPtr scene, const std::filesystem::path& filepath); void OnScenePlay(); void OnSceneSimulate(); @@ -60,12 +60,12 @@ namespace StarEngine bool m_ViewportFocused = false, m_ViewportHovered = false; // Temp - Ref m_SquareVA; - Ref m_FlatColorShader; - Ref m_Framebuffer; + RefPtr m_SquareVA; + RefPtr m_FlatColorShader; + RefPtr m_Framebuffer; - Ref m_ActiveScene; - Ref m_EditorScene; + RefPtr m_ActiveScene; + RefPtr m_EditorScene; std::filesystem::path m_EditorScenePath; Entity m_SquareEntity; Entity m_CameraEntity; @@ -77,7 +77,7 @@ namespace StarEngine EditorCamera m_EditorCamera; - Ref m_CheckerboardTexture; + RefPtr m_CheckerboardTexture; glm::vec2 m_ViewportSize = {0.0f, 0.0f}; glm::vec2 m_ViewportBounds[2]; @@ -96,10 +96,10 @@ namespace StarEngine // Panels SceneHierarchyPanel m_SceneHierarchyPanel; - Scope m_ContentBrowserPanel; + std::unique_ptr m_ContentBrowserPanel; // Editor resources - Ref m_IconPlay, m_IconPause, m_IconStep, m_IconSimulate, m_IconStop; + RefPtr m_IconPlay, m_IconPause, m_IconStep, m_IconSimulate, m_IconStop; }; } diff --git a/StarEditor/src/Panels/ContentBrowserPanel.cpp b/StarEditor/src/Panels/ContentBrowserPanel.cpp index 9d7a117a..530e73cb 100644 --- a/StarEditor/src/Panels/ContentBrowserPanel.cpp +++ b/StarEditor/src/Panels/ContentBrowserPanel.cpp @@ -9,8 +9,8 @@ namespace StarEngine { - ContentBrowserPanel::ContentBrowserPanel(Ref project) - : m_Project(project), m_ThumbnailCache(CreateRef(project)), m_BaseDirectory(m_Project->GetAssetDirectory()), m_CurrentDirectory(m_BaseDirectory) + ContentBrowserPanel::ContentBrowserPanel(RefPtr project) + : m_Project(project), m_ThumbnailCache(RefPtr::Create(project)), m_BaseDirectory(m_Project->GetAssetDirectory()), m_CurrentDirectory(m_BaseDirectory) { m_TreeNodes.push_back(TreeNode(".", 0)); @@ -86,7 +86,7 @@ namespace StarEngine { std::string itemStr = item.generic_string(); ImGui::PushID(itemStr.c_str()); - Ref icon = isDirectory ? m_DirectoryIcon : m_FileIcon; + RefPtr icon = isDirectory ? m_DirectoryIcon : m_FileIcon; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::ImageButton("##icon", (ImTextureID)icon->GetRendererID(), { thumbnailSize, thumbnailSize }, { 0, 1 }, { 1, 0 }); @@ -163,7 +163,7 @@ namespace StarEngine { // THUMBNAIL auto relativePath = std::filesystem::relative(path, Project::GetActiveAssetDirectory()); - Ref thumbnail = m_DirectoryIcon; + RefPtr thumbnail = m_DirectoryIcon; if (!directoryEntry.is_directory()) { thumbnail = m_ThumbnailCache->GetOrCreateThumbnail(relativePath); @@ -236,7 +236,7 @@ namespace StarEngine { // THUMBNAIL auto relativePath = std::filesystem::relative(path, Project::GetActiveAssetDirectory()); - Ref thumbnail = m_DirectoryIcon; + RefPtr thumbnail = m_DirectoryIcon; if (!directoryEntry.is_directory()) { thumbnail = m_ThumbnailCache->GetOrCreateThumbnail(relativePath); diff --git a/StarEditor/src/Panels/ContentBrowserPanel.h b/StarEditor/src/Panels/ContentBrowserPanel.h index ed1f05a7..53648533 100644 --- a/StarEditor/src/Panels/ContentBrowserPanel.h +++ b/StarEditor/src/Panels/ContentBrowserPanel.h @@ -10,23 +10,23 @@ namespace StarEngine { - class ContentBrowserPanel + class ContentBrowserPanel : public RefCounted { public: - ContentBrowserPanel(Ref project); + ContentBrowserPanel(RefPtr project); void OnImGuiRender(); private: void RefreshAssetTree(); private: - Ref m_Project; - Ref m_ThumbnailCache; + RefPtr m_Project; + RefPtr m_ThumbnailCache; std::filesystem::path m_BaseDirectory; std::filesystem::path m_CurrentDirectory; - Ref m_DirectoryIcon; - Ref m_FileIcon; + RefPtr m_DirectoryIcon; + RefPtr m_FileIcon; struct TreeNode { diff --git a/StarEditor/src/Panels/SceneHierarchyPanel.cpp b/StarEditor/src/Panels/SceneHierarchyPanel.cpp index e3e190fa..f0bef472 100644 --- a/StarEditor/src/Panels/SceneHierarchyPanel.cpp +++ b/StarEditor/src/Panels/SceneHierarchyPanel.cpp @@ -26,12 +26,12 @@ namespace StarEngine { - SceneHierarchyPanel::SceneHierarchyPanel(const Ref& context) + SceneHierarchyPanel::SceneHierarchyPanel(const RefPtr& context) { SetContext(context); } - void SceneHierarchyPanel::SetContext(const Ref& context) + void SceneHierarchyPanel::SetContext(const RefPtr& context) { m_Context = context; m_SelectionContext = {}; @@ -380,7 +380,7 @@ namespace StarEngine { bool sceneRunning = scene->IsRunning(); if (sceneRunning) { - Ref scriptInstance = ScriptEngine::GetEntityScriptInstance(entity.GetUUID()); + RefPtr scriptInstance = ScriptEngine::GetEntityScriptInstance(entity.GetUUID()); if (scriptInstance) { const auto& fields = scriptInstance->GetScriptClass()->GetFields(); @@ -401,7 +401,7 @@ namespace StarEngine { { if (scriptClassExists) { - Ref entityClass = ScriptEngine::GetEntityClass(component.ClassName); + RefPtr entityClass = ScriptEngine::GetEntityClass(component.ClassName); const auto& fields = entityClass->GetFields(); auto& entityFields = ScriptEngine::GetScriptFieldMap(entity); @@ -632,7 +632,7 @@ namespace StarEngine { if (component.Audio != 0) { - Ref audioSource = AssetManager::GetAsset(component.Audio); + RefPtr audioSource = AssetManager::GetAsset(component.Audio); float volumeMultiplier = config.VolumeMultiplier; if (ImGui::SliderFloat("Volume Multiplier", &config.VolumeMultiplier, 0.0f, 2.0f, "%.2f")) diff --git a/StarEditor/src/Panels/SceneHierarchyPanel.h b/StarEditor/src/Panels/SceneHierarchyPanel.h index 2fdba695..9ba89a60 100644 --- a/StarEditor/src/Panels/SceneHierarchyPanel.h +++ b/StarEditor/src/Panels/SceneHierarchyPanel.h @@ -1,6 +1,6 @@ #pragma once -#include "StarEngine/Core/Base.h" +#include "StarEngine/Core/Ref.h" #include "StarEngine/Scene/Scene.h" #include "StarEngine/Scene/Entity.h" @@ -10,9 +10,9 @@ namespace StarEngine { { public: SceneHierarchyPanel() = default; - SceneHierarchyPanel(const Ref& scene); + SceneHierarchyPanel(const RefPtr& scene); - void SetContext(const Ref& scene); + void SetContext(const RefPtr& scene); void OnImGuiRender(); @@ -25,7 +25,7 @@ namespace StarEngine { void DrawEntityNode(Entity entity); void DrawComponents(Entity entity); private: - Ref m_Context; + RefPtr m_Context; Entity m_SelectionContext; }; diff --git a/StarEditor/src/Panels/ThumbnailCache.cpp b/StarEditor/src/Panels/ThumbnailCache.cpp index 6b595a43..058a2a8f 100644 --- a/StarEditor/src/Panels/ThumbnailCache.cpp +++ b/StarEditor/src/Panels/ThumbnailCache.cpp @@ -1,3 +1,4 @@ +#include "sepch.h" #include "ThumbnailCache.h" #include "StarEngine/Asset/TextureImporter.h" diff --git a/StarEditor/src/Panels/ThumbnailCache.h b/StarEditor/src/Panels/ThumbnailCache.h index 5b5d394d..e3b714f8 100644 --- a/StarEditor/src/Panels/ThumbnailCache.h +++ b/StarEditor/src/Panels/ThumbnailCache.h @@ -10,18 +10,18 @@ namespace StarEngine { struct ThumbnailImage { uint64_t Timestamp; - Ref Image; + RefPtr Image; }; - class ThumbnailCache + class ThumbnailCache : public RefCounted { public: - ThumbnailCache(Ref project); + ThumbnailCache(RefPtr project); - Ref GetOrCreateThumbnail(const std::filesystem::path& path); + RefPtr GetOrCreateThumbnail(const std::filesystem::path& path); void OnUpdate(); private: - Ref m_Project; + RefPtr m_Project; uint32_t m_ThumbnailSize = 128; diff --git a/StarEditor/src/StarEditorApp.cpp b/StarEditor/src/StarEditorApp.cpp index c55bbf03..d4dd9952 100644 --- a/StarEditor/src/StarEditorApp.cpp +++ b/StarEditor/src/StarEditorApp.cpp @@ -1,6 +1,7 @@ +#include "sepch.h" #include -#include +#include #include "EditorLayer.h" @@ -24,6 +25,10 @@ namespace StarEngine { { ApplicationSpecification spec; spec.Name = "StarEditor"; + //spec.WindowWidth = 1600; + //spec.WindowHeight = 900; + //spec.StartMaximized = true; + //spec.VSync = true; spec.CommandLineArgs = args; return new StarEditor(spec);; diff --git a/StarEngine-ScriptCore/premake5-dotnet.lua b/StarEngine-ScriptCore/premake5-dotnet.lua new file mode 100644 index 00000000..48e63554 --- /dev/null +++ b/StarEngine-ScriptCore/premake5-dotnet.lua @@ -0,0 +1,43 @@ +-- Non-Windows split premake project + +StarEngineRootDirectory = os.getenv("STARENGINE_DIR") +include (path.join(StarEngineRootDirectory, "StarEngine", "vendor", "Coral", "Premake", "CSExtensions.lua")) + +workspace "StarEngine-ScriptCore" + configurations { "Debug", "Release" } + + filter "configurations:Debug or configurations:Debug-AS" + optimize "Off" + symbols "On" + + filter "configurations:Release" + optimize "On" + symbols "Default" + + filter "configurations:Dist" + optimize "Full" + symbols "Off" + + include "../StarEngine/vendor/Coral/Coral.Managed" + + project "StarEngine-ScriptCore" + kind "SharedLib" + language "C#" + dotnetframework "net8.0" + clr "Unsafe" + targetdir ("../StarEditor/Resources/Scripts") + objdir ("../StarEditor/Resources/Scripts/Intermediates") + + --linkAppReferences(false) + + --links { "Coral.Managed" } + + propertytags { + { "AppendTargetFrameworkToOutputPath", "false" }, + { "Nullable", "enable" }, + } + + files { + "Source/**.cs", + "Properties/**.cs" + } diff --git a/StarEngine-ScriptCore/premake5.lua b/StarEngine-ScriptCore/premake5.lua index b92a25b4..dcc17fb3 100644 --- a/StarEngine-ScriptCore/premake5.lua +++ b/StarEngine-ScriptCore/premake5.lua @@ -1,25 +1,21 @@ +StarEngineRootDirectory = os.getenv("STARENGINE_DIR") + +--include (path.join(StarEngineRootDirectory, "StarEngine", "vendor", "Coral", "Premake", "CSExtensions.lua")) +--include (path.join(StarEngineRootDirectory, "StarEngine", "vendor", "Coral", "Coral.Managed")) + project "StarEngine-ScriptCore" kind "SharedLib" language "C#" - dotnetframework "4.7.2" - - targetdir ("../StarEditor/Resources/Scripts") - objdir ("../StarEditor/Resources/Scripts/Intermediates") + dotnetframework "net8.0" + clr "Unsafe" + targetdir "%{StarEngineRootDirectory}/StarEditor/Resources/Scripts" + objdir "%{StarEngineRootDirectory}/StarEditor/Resources/Scripts/Intermediates" - files - { - "Source/**.cs", - "Properties/**.cs" - } - - filter "configurations:Debug" - optimize "Off" - symbols "Default" + --links { "Coral.Managed" } - filter "configurations:Release" - optimize "On" - symbols "Default" + --propertytags {{ "AppendTargetFrameworkToOutputPath", "false" },{ "Nullable", "enable" },} - filter "configurations:Dist" - optimize "Full" - symbols "Off" + files { + "%{StarEngineRootDirectory}/StarEngine-ScriptCore/Source/**.cs", + "%{StarEngineRootDirectory}/StarEngine-ScriptCore/Properties/**.cs" + } \ No newline at end of file diff --git a/StarEngine/Platform/Windows/WindowsFileSystem.cpp b/StarEngine/Platform/Windows/WindowsFileSystem.cpp new file mode 100644 index 00000000..14462ce4 --- /dev/null +++ b/StarEngine/Platform/Windows/WindowsFileSystem.cpp @@ -0,0 +1,135 @@ +#include "sepch.h" +#include "StarEngine/Utilities/FileSystem.h" +#include "StarEngine/Asset/AssetManager.h" + +#include "StarEngine/Core/Application.h" + +#include + +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +#include +#include + +#include +#include + +namespace StarEngine { + + static std::filesystem::path s_PersistentStoragePath; + + FileStatus FileSystem::TryOpenFile(const std::filesystem::path& filepath) + { + HANDLE fileHandle = CreateFile(filepath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr); + if (fileHandle == INVALID_HANDLE_VALUE) + { + DWORD errorCode = GetLastError(); + if (errorCode == ERROR_FILE_NOT_FOUND || errorCode == ERROR_PATH_NOT_FOUND) + return FileStatus::Invalid; + if (errorCode == ERROR_SHARING_VIOLATION) + return FileStatus::Locked; + + return FileStatus::OtherError; + } + + CloseHandle(fileHandle); + return FileStatus::Success; + } + + bool FileSystem::WriteBytes(const std::filesystem::path& filepath, const Buffer& buffer) + { + std::ofstream stream(filepath, std::ios::binary | std::ios::trunc); + + if (!stream) + { + stream.close(); + return false; + } + + stream.write((char*)buffer.Data, buffer.Size); + stream.close(); + + return true; + } + + Buffer FileSystem::ReadBytes(const std::filesystem::path& filepath) + { + Buffer buffer; + + std::ifstream stream(filepath, std::ios::binary | std::ios::ate); + SE_CORE_ASSERT(stream); + + auto end = stream.tellg(); + stream.seekg(0, std::ios::beg); + auto size = end - stream.tellg(); + SE_CORE_ASSERT(size != 0); + + buffer.Allocate((uint32_t)size); + stream.read((char*)buffer.Data, buffer.Size); + stream.close(); + + return buffer; + } + + std::filesystem::path FileSystem::GetPersistentStoragePath() + { + if (!s_PersistentStoragePath.empty()) + return s_PersistentStoragePath; + + PWSTR roamingFilePath; + HRESULT result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, NULL, &roamingFilePath); + SE_CORE_VERIFY(result == S_OK); + s_PersistentStoragePath = roamingFilePath; + s_PersistentStoragePath /= "StarEngine"; + + if (!std::filesystem::exists(s_PersistentStoragePath)) + std::filesystem::create_directory(s_PersistentStoragePath); + + return s_PersistentStoragePath; + } + + bool FileSystem::HasEnvironmentVariable(const std::string& key) + { + HKEY hKey; + LSTATUS lOpenStatus = RegOpenKeyExA(HKEY_CURRENT_USER, "Environment", 0, KEY_ALL_ACCESS, &hKey); + + if (lOpenStatus == ERROR_SUCCESS) + { + lOpenStatus = RegQueryValueExA(hKey, key.c_str(), 0, NULL, NULL, NULL); + RegCloseKey(hKey); + } + + return lOpenStatus == ERROR_SUCCESS; + } + + bool FileSystem::SetEnvironmentVariable(const std::string& key, const std::string& value) + { + HKEY hKey; + LPCSTR keyPath = "Environment"; + DWORD createdNewKey; + LSTATUS lOpenStatus = RegCreateKeyExA(HKEY_CURRENT_USER, keyPath, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, &createdNewKey); + if (lOpenStatus == ERROR_SUCCESS) + { + LSTATUS lSetStatus = RegSetValueExA(hKey, key.c_str(), 0, REG_SZ, (LPBYTE)value.c_str(), (DWORD)(value.length() + 1)); + RegCloseKey(hKey); + + if (lSetStatus == ERROR_SUCCESS) + { + SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)"Environment", SMTO_BLOCK, 100, NULL); + return true; + } + } + + return false; + } + + std::string FileSystem::GetEnvironmentVariable(const std::string& key) + { + const char* value = getenv(key.c_str()); + if (value) + return std::string(value); + else + return {}; + } + +} diff --git a/StarEngine/Platform/Windows/WindowsProcessHelper.cpp b/StarEngine/Platform/Windows/WindowsProcessHelper.cpp new file mode 100644 index 00000000..f1593767 --- /dev/null +++ b/StarEngine/Platform/Windows/WindowsProcessHelper.cpp @@ -0,0 +1,80 @@ +#include "sepch.h" +#include "StarEngine/Utilities/ProcessHelper.h" + +// NOTE(Peter): codecvt has *technically* been deprecated, but the C++ committee has said it's still safe and portable to use until a suitable replacement has been found +#include + +namespace StarEngine { + + static std::unordered_map s_WindowsProcessStorage; + + UUID ProcessHelper::CreateProcess(const ProcessInfo& inProcessInfo) + { + std::filesystem::path workingDirectory = inProcessInfo.WorkingDirectory.empty() ? inProcessInfo.FilePath.parent_path() : inProcessInfo.WorkingDirectory; + + std::wstring commandLine = inProcessInfo.IncludeFilePathInCommands ? inProcessInfo.FilePath.wstring() : L""; + + if (!inProcessInfo.CommandLine.empty()) + { + if (commandLine.size() > 0) + { + std::wstring_convert> wstringConverter; + commandLine += L" " + wstringConverter.from_bytes(inProcessInfo.CommandLine); + } + else + { + std::wstring_convert> wstringConverter; + commandLine = wstringConverter.from_bytes(inProcessInfo.CommandLine); + } + } + + PROCESS_INFORMATION processInformation; + ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION)); + + STARTUPINFO startupInfo; + ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); + startupInfo.cb = sizeof(STARTUPINFO); + + DWORD creationFlags = NORMAL_PRIORITY_CLASS; + + if (inProcessInfo.Detached) + creationFlags |= DETACHED_PROCESS; + + BOOL success = ::CreateProcess( + inProcessInfo.FilePath.c_str(), commandLine.data(), + NULL, NULL, FALSE, creationFlags, NULL, + workingDirectory.c_str(), &startupInfo, &processInformation); + + if (!success) + { + CloseHandle(processInformation.hThread); + CloseHandle(processInformation.hProcess); + return 0; + } + + UUID processID = UUID(); + + if (inProcessInfo.Detached) + { + CloseHandle(processInformation.hThread); + CloseHandle(processInformation.hProcess); + } + else + { + s_WindowsProcessStorage[processID] = processInformation; + } + + return processID; + } + + void ProcessHelper::DestroyProcess(UUID inHandle, uint32_t inExitCode) + { + SE_CORE_VERIFY(s_WindowsProcessStorage.find(inHandle) != s_WindowsProcessStorage.end(), "Trying to destroy untracked process!"); + const auto& processInformation = s_WindowsProcessStorage[inHandle]; + TerminateProcess(processInformation.hProcess, inExitCode); + CloseHandle(processInformation.hThread); + CloseHandle(processInformation.hProcess); + s_WindowsProcessStorage.erase(inHandle); + } + +} diff --git a/StarEngine/Platform/Windows/WindowsRenderThread.cpp b/StarEngine/Platform/Windows/WindowsRenderThread.cpp new file mode 100644 index 00000000..9e4e0d52 --- /dev/null +++ b/StarEngine/Platform/Windows/WindowsRenderThread.cpp @@ -0,0 +1,141 @@ +#include "sepch.h" +#include "StarEngine/Core/RenderThread.h" + +#include "StarEngine/Renderer/Renderer.h" + +#include + +namespace StarEngine { + + struct RenderThreadData + { + CRITICAL_SECTION m_CriticalSection; + CONDITION_VARIABLE m_ConditionVariable; + + RenderThread::State m_State = RenderThread::State::Idle; + }; + + static std::thread::id s_RenderThreadID; + + RenderThread::RenderThread(ThreadingPolicy coreThreadingPolicy) + : m_RenderThread("Render Thread"), m_ThreadingPolicy(coreThreadingPolicy) + { + m_Data = new RenderThreadData(); + + if (m_ThreadingPolicy == ThreadingPolicy::MultiThreaded) + { + InitializeCriticalSection(&m_Data->m_CriticalSection); + InitializeConditionVariable(&m_Data->m_ConditionVariable); + } + } + + RenderThread::~RenderThread() + { + if (m_ThreadingPolicy == ThreadingPolicy::MultiThreaded) + DeleteCriticalSection(&m_Data->m_CriticalSection); + + s_RenderThreadID = std::thread::id(); + } + + void RenderThread::Run() + { + m_IsRunning = true; + if (m_ThreadingPolicy == ThreadingPolicy::MultiThreaded) + m_RenderThread.Dispatch(Renderer::RenderThreadFunc, this); + + s_RenderThreadID = m_RenderThread.GetID(); + } + + void RenderThread::Terminate() + { + m_IsRunning = false; + Pump(); + + if (m_ThreadingPolicy == ThreadingPolicy::MultiThreaded) + m_RenderThread.Join(); + + s_RenderThreadID = std::thread::id(); + } + + void RenderThread::Wait(State waitForState) + { + if (m_ThreadingPolicy == ThreadingPolicy::SingleThreaded) + return; + + EnterCriticalSection(&m_Data->m_CriticalSection); + while (m_Data->m_State != waitForState) + { + // This releases the CS so that another thread can wake it + SleepConditionVariableCS(&m_Data->m_ConditionVariable, &m_Data->m_CriticalSection, INFINITE); + } + LeaveCriticalSection(&m_Data->m_CriticalSection); + } + + void RenderThread::WaitAndSet(State waitForState, State setToState) + { + if (m_ThreadingPolicy == ThreadingPolicy::SingleThreaded) + return; + + EnterCriticalSection(&m_Data->m_CriticalSection); + while (m_Data->m_State != waitForState) + { + SleepConditionVariableCS(&m_Data->m_ConditionVariable, &m_Data->m_CriticalSection, INFINITE); + } + m_Data->m_State = setToState; + WakeAllConditionVariable(&m_Data->m_ConditionVariable); + LeaveCriticalSection(&m_Data->m_CriticalSection); + } + + void RenderThread::Set(State setToState) + { + if (m_ThreadingPolicy == ThreadingPolicy::SingleThreaded) + return; + + EnterCriticalSection(&m_Data->m_CriticalSection); + m_Data->m_State = setToState; + WakeAllConditionVariable(&m_Data->m_ConditionVariable); + LeaveCriticalSection(&m_Data->m_CriticalSection); + } + + void RenderThread::NextFrame() + { + m_AppThreadFrame++; + Renderer::SwapQueues(); + } + + void RenderThread::BlockUntilRenderComplete() + { + if (m_ThreadingPolicy == ThreadingPolicy::SingleThreaded) + return; + + Wait(State::Idle); + } + + void RenderThread::Kick() + { + if (m_ThreadingPolicy == ThreadingPolicy::MultiThreaded) + { + Set(State::Kick); + } + else + { + Renderer::WaitAndRender(this); + } + } + + void RenderThread::Pump() + { + NextFrame(); + Kick(); + BlockUntilRenderComplete(); + } + + bool RenderThread::IsCurrentThreadRT() + { + // NOTE(Yan): for debugging + // SE_CORE_VERIFY(s_RenderThreadID != std::thread::id()); + return s_RenderThreadID == std::this_thread::get_id(); + } + + +} diff --git a/StarEngine/Platform/Windows/WindowsScriptBuilder.cpp b/StarEngine/Platform/Windows/WindowsScriptBuilder.cpp new file mode 100644 index 00000000..690401fe --- /dev/null +++ b/StarEngine/Platform/Windows/WindowsScriptBuilder.cpp @@ -0,0 +1,24 @@ +#include "sepch.h" +//#include "StarEngine/Scripting/ScriptBuilder.h" + +#include + +#include + +namespace StarEngine { + /* + void ScriptBuilder::BuildCSProject(const std::filesystem::path& filepath) + { + TCHAR programFilesFilePath[MAX_PATH]; + SHGetSpecialFolderPath(0, programFilesFilePath, CSIDL_PROGRAM_FILES, FALSE); + std::filesystem::path msBuildPath = std::filesystem::path(programFilesFilePath) / "Microsoft Visual Studio" / "2022" / "Community" / "Msbuild" / "Current" / "Bin" / "MSBuild.exe"; + std::string command = std::format("cd \"{}\" && \"{}\" \"{}\" -property:Configuration=Debug -t:restore,build", filepath.parent_path().string(), msBuildPath.string(), filepath.filename().string()); + system(command.c_str()); + } + + void ScriptBuilder::BuildScriptAssembly(Ref project) + { + BuildCSProject(project->GetScriptProjectPath()); + } + */ +} diff --git a/StarEngine/Platform/Windows/WindowsThread.cpp b/StarEngine/Platform/Windows/WindowsThread.cpp new file mode 100644 index 00000000..7cd4e455 --- /dev/null +++ b/StarEngine/Platform/Windows/WindowsThread.cpp @@ -0,0 +1,55 @@ +#include "sepch.h" +#include "StarEngine/Core/Thread.h" + +#define GLFW_EXPOSE_NATIVE_WIN32 +#include + +namespace StarEngine { + + Thread::Thread(const std::string& name) + : m_Name(name) + { + } + + void Thread::SetName(const std::string& name) + { + HANDLE threadHandle = m_Thread.native_handle(); + + std::wstring wName(name.begin(), name.end()); + SetThreadDescription(threadHandle, wName.c_str()); + SetThreadAffinityMask(threadHandle, 8); + } + + void Thread::Join() + { + if (m_Thread.joinable()) + m_Thread.join(); + } + + ThreadSignal::ThreadSignal(const std::string& name, bool manualReset) + { + std::wstring str(name.begin(), name.end()); + m_SignalHandle = CreateEvent(NULL, (BOOL)manualReset, FALSE, str.c_str()); + } + + void ThreadSignal::Wait() + { + WaitForSingleObject(m_SignalHandle, INFINITE); + } + + void ThreadSignal::Signal() + { + SetEvent(m_SignalHandle); + } + + void ThreadSignal::Reset() + { + ResetEvent(m_SignalHandle); + } + + std::thread::id Thread::GetID() const + { + return m_Thread.get_id(); + } + +} diff --git a/StarEngine/premake5.lua b/StarEngine/premake5.lua index 86745aed..5318bae9 100644 --- a/StarEngine/premake5.lua +++ b/StarEngine/premake5.lua @@ -1,124 +1,119 @@ project "StarEngine" kind "StaticLib" - language "C++" - cppdialect "C++17" - staticruntime "off" + --dependson "Coral.Managed" - targetdir ("%{wks.location}/bin/" .. outputdir .. "/%{prj.name}") - objdir ("%{wks.location}/bin-int/" .. outputdir .. "/%{prj.name}") + debuggertype "NativeWithManagedCore" + + targetdir ("../bin/" .. outputdir .. "/%{prj.name}") + objdir ("../bin-int/" .. outputdir .. "/%{prj.name}") pchheader "sepch.h" pchsource "src/sepch.cpp" - files - { + files { "src/**.h", + "src/**.c", + "src/**.hpp", "src/**.cpp", - "vendor/stb_image/**.h", - "vendor/stb_image/**.cpp", - "vendor/glm/glm/**.hpp", - "vendor/glm/glm/**.inl", - "vendor/imguizmo/ImGuizmo.h", - "vendor/imguizmo/ImGuizmo.cpp" - } + "Platform/" .. firstToUpper(os.target()) .. "/**.hpp", + "Platform/" .. firstToUpper(os.target()) .. "/**.cpp", - defines - { - "_CRT_SECURE_NO_WARNINGS", - "GLFW_INCLUDE_NONE", - "YAML_CPP_STATIC_DEFINE" - } + "vendor/FastNoise/**.cpp", - includedirs - { - "src", - "vendor/spdlog/include", - "%{IncludeDir.GLFW}", - "%{IncludeDir.GLAD}", - "%{IncludeDir.Box2D}", - "%{IncludeDir.ImGui}", - "%{IncludeDir.glm}", - "%{IncludeDir.filewatch}", - "%{IncludeDir.stb_image}", - "%{IncludeDir.entt}", - "%{IncludeDir.msdfgen}", - "%{IncludeDir.msdf_atlas_gen}", - "%{IncludeDir.yaml_cpp}", - "%{IncludeDir.ImGuizmo}", - "%{IncludeDir.VulkanSDK}", - - "%{IncludeDir.mono}", - "%{IncludeDir.miniaudio}" - } + "vendor/yaml-cpp/src/**.cpp", + "vendor/yaml-cpp/src/**.h", + "vendor/yaml-cpp/include/**.h", + "vendor/VulkanMemoryAllocator/**.h", + "vendor/VulkanMemoryAllocator/**.cpp", - links - { - "GLFW", - "GLAD", - "imgui", - "opengl32", - "%{Library.mono}", - "yaml-cpp", - "msdf-atlas-gen", - "Box2D", - "dwmapi.lib" + "vendor/imgui/misc/cpp/imgui_stdlib.cpp", + "vendor/imgui/misc/cpp/imgui_stdlib.h" } + + removefiles { + "src/StarEngine/Platform/DX11/**.cpp", + "src/StarEngine/Platform/DX12/**.cpp", + } + + includedirs { "src/", "vendor/", } + + IncludeDependencies() - filter "files:vendor/imguizmo/**.cpp" + defines { "GLM_FORCE_DEPTH_ZERO_TO_ONE" } + + filter "files:vendor/FastNoise/**.cpp or files:vendor/yaml-cpp/src/**.cpp or files:vendor/imgui/misc/cpp/imgui_stdlib.cpp or files:src/StarEngine/Tiering/TieringSerializer.cpp or files:src/StarEngine/Core/ApplicationSettings.cpp" flags { "NoPCH" } filter "system:windows" systemversion "latest" + defines { "SE_PLATFORM_WINDOWS", } - defines - { - - } + filter "system:linux" + defines { "SE_PLATFORM_LINUX", "__EMULATE_UUID", "BACKWARD_HAS_DW", "BACKWARD_HAS_LIBUNWIND" } + links { "dw", "dl", "unwind", "pthread" } - links - { - "%{Library.WinSock}", - "%{Library.WinMM}", - "%{Library.WinVersion}", - "%{Library.BCrypt}", - } + filter "configurations:Debug or configurations:Debug-AS" + symbols "On" + defines { "SE_DEBUG", "_DEBUG", "ACL_ON_ASSERT_ABORT", } - filter "configurations:Debug" - defines "SE_DEBUG" - runtime "Debug" - symbols "on" - - links - { - "%{Library.ShaderC_Debug}", - "%{Library.SPIRV_Cross_Debug}", - "%{Library.SPIRV_Cross_GLSL_Debug}" - } + filter { "system:windows", "configurations:Debug-AS" } + sanitize { "Address" } + flags { "NoRuntimeChecks", "NoIncrementalLink" } filter "configurations:Release" - defines "SE_RELEASE" - runtime "Release" - optimize "on" - - links - { - "%{Library.ShaderC_Release}", - "%{Library.SPIRV_Cross_Release}", - "%{Library.SPIRV_Cross_GLSL_Release}" + optimize "On" + vectorextensions "AVX2" + isaextensions { "BMI", "POPCNT", "LZCNT", "F16C" } + defines { "SE_RELEASE", "NDEBUG", } + + filter { "configurations:Debug or configurations:Debug-AS or configurations:Release" } + defines { + "SE_TRACK_MEMORY", + + "JPH_DEBUG_RENDERER", + "JPH_FLOATING_POINT_EXCEPTIONS_ENABLED", + "JPH_EXTERNAL_PROFILE" } filter "configurations:Dist" - defines "SE_DIST" - runtime "Release" - optimize "on" - - links - { - "%{Library.ShaderC_Release}", - "%{Library.SPIRV_Cross_Release}", - "%{Library.SPIRV_Cross_GLSL_Release}" + optimize "On" + symbols "Off" + vectorextensions "AVX2" + isaextensions { "BMI", "POPCNT", "LZCNT", "F16C" } + defines { "SE_DIST" } + + removefiles { + "src/StarEngine/Platform/Vulkan/ShaderCompiler/**.cpp", + "src/StarEngine/Platform/Vulkan/Debug/**.cpp", + + "src/StarEngine/Asset/AssimpAnimationImporter.cpp", + "src/StarEngine/Asset/AssimpMeshImporter.cpp", } - - filter "action:vs2022" - buildoptions { "/utf-8" } + + filter { "configurations:Debug-AS" } + postbuildcommands { + '{MKDIR} "%{wks.location}/StarEditor/DotNet"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Coral.Managed/Coral.Managed.runtimeconfig.json" "%{wks.location}/StarEditor/DotNet/Coral.Managed.runtimeconfig.json"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Coral.Managed/Build/Debug-AS-%{cfg.system}/Coral.Managed.dll" "%{wks.location}/StarEditor/DotNet/Coral.Managed.dll"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Coral.Managed/Build/Debug-AS-%{cfg.system}/Coral.Managed.pdb" "%{wks.location}/StarEditor/DotNet/Coral.Managed.pdb"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Coral.Managed/Build/Debug-AS-%{cfg.system}/Coral.Managed.deps.json" "%{wks.location}/StarEditor/DotNet/Coral.Managed.deps.json"', + } + + filter { "configurations:Debug" } + postbuildcommands { + '{MKDIR} "%{wks.location}/StarEditor/DotNet"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Coral.Managed/Coral.Managed.runtimeconfig.json" "%{wks.location}/StarEditor/DotNet/Coral.Managed.runtimeconfig.json"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Build/Debug/Coral.Managed.dll" "%{wks.location}/StarEditor/DotNet/Coral.Managed.dll"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Build/Debug/Coral.Managed.pdb" "%{wks.location}/StarEditor/DotNet/Coral.Managed.pdb"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Build/Debug/Coral.Managed.deps.json" "%{wks.location}/HStarEditorazelnut/DotNet/Coral.Managed.deps.json"', + } + + filter { "configurations:Release" } + postbuildcommands { + '{MKDIR} "%{wks.location}/StarEditor/DotNet"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Coral.Managed/Coral.Managed.runtimeconfig.json" "%{wks.location}/StarEditor/DotNet/Coral.Managed.runtimeconfig.json"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Build/Release/Coral.Managed.dll" "%{wks.location}/StarEditor/DotNet/Coral.Managed.dll"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Build/Release/Coral.Managed.pdb" "%{wks.location}/StarEditor/DotNet/Coral.Managed.pdb"', + '{COPYFILE} "%{wks.location}/StarEngine/vendor/Coral/Build/Release/Coral.Managed.deps.json" "%{wks.location}/StarEditor/DotNet/Coral.Managed.deps.json"', + } diff --git a/StarEngine/src/Platform/OpenGL/OpenGLBuffer.cpp b/StarEngine/src/Platform/OpenGL/OpenGLBuffer.cpp deleted file mode 100644 index f19891ae..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLBuffer.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include "sepch.h" -#include "Platform/OpenGL/OpenGLBuffer.h" - -#include - -namespace StarEngine { - - ///////////////////////////////////////////////////////////////////////////// - // VertexBuffer ///////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////// - - OpenGLVertexBuffer::OpenGLVertexBuffer(uint32_t size) - { - SE_PROFILE_FUNCTION(); - glCreateBuffers(1, &m_RendererID); - glBindBuffer(GL_ARRAY_BUFFER, m_RendererID); - glBufferData(GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_DRAW); - } - - OpenGLVertexBuffer::OpenGLVertexBuffer(float* vertices, uint32_t size) - { - SE_PROFILE_FUNCTION(); - - glCreateBuffers(1, &m_RendererID); - glBindBuffer(GL_ARRAY_BUFFER, m_RendererID); - glBufferData(GL_ARRAY_BUFFER, size, vertices, GL_STATIC_DRAW); - } - - OpenGLVertexBuffer::~OpenGLVertexBuffer() - { - SE_PROFILE_FUNCTION(); - - glDeleteBuffers(1, &m_RendererID); - } - - void OpenGLVertexBuffer::Bind() const - { - SE_PROFILE_FUNCTION(); - - glBindBuffer(GL_ARRAY_BUFFER, m_RendererID); - } - - void OpenGLVertexBuffer::Unbind() const - { - SE_PROFILE_FUNCTION(); - - glBindBuffer(GL_ARRAY_BUFFER, 0); - } - - void OpenGLVertexBuffer::SetData(const void* data, uint32_t size) - { - glBindBuffer(GL_ARRAY_BUFFER, m_RendererID); - glBufferSubData(GL_ARRAY_BUFFER, 0, size, data); - } - - ///////////////////////////////////////////////////////////////////////////// - // IndexBuffer ////////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////// - - OpenGLIndexBuffer::OpenGLIndexBuffer(uint32_t* indices, uint32_t count) - : m_Count(count) - { - SE_PROFILE_FUNCTION(); - - glCreateBuffers(1, &m_RendererID); - - // GL_ELEMENT_ARRAY_BUFFER is not valid without an actively bound VAO - // Binding with GL_ARRAY_BUFFER allows the data to be loaded regardless of VAO state. - glBindBuffer(GL_ARRAY_BUFFER, m_RendererID); - glBufferData(GL_ARRAY_BUFFER, count * sizeof(uint32_t), indices, GL_STATIC_DRAW); - } - - OpenGLIndexBuffer::~OpenGLIndexBuffer() - { - SE_PROFILE_FUNCTION(); - - glDeleteBuffers(1, &m_RendererID); - } - - void OpenGLIndexBuffer::Bind() const - { - SE_PROFILE_FUNCTION(); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID); - } - - void OpenGLIndexBuffer::Unbind() const - { - SE_PROFILE_FUNCTION(); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - } -} \ No newline at end of file diff --git a/StarEngine/src/Platform/OpenGL/OpenGLBuffer.h b/StarEngine/src/Platform/OpenGL/OpenGLBuffer.h deleted file mode 100644 index a9491749..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLBuffer.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/Buffer.h" - -namespace StarEngine { - - class OpenGLVertexBuffer : public VertexBuffer { - - public: - OpenGLVertexBuffer(uint32_t size); - OpenGLVertexBuffer(float* vertices, uint32_t size); - virtual ~OpenGLVertexBuffer(); - - virtual void Bind() const override; - virtual void Unbind() const override; - - virtual void SetData(const void* data, uint32_t size) override; - - virtual const BufferLayout& GetLayout() const override { return m_Layout; } - virtual void SetLayout(const BufferLayout& layout) override { m_Layout = layout; } - private: - uint32_t m_RendererID; - BufferLayout m_Layout; - }; - - class OpenGLIndexBuffer : public IndexBuffer { - - public: - OpenGLIndexBuffer(uint32_t* indices, uint32_t count); - virtual ~OpenGLIndexBuffer(); - - virtual void Bind() const; - virtual void Unbind() const; - - virtual uint32_t GetCount() const { return m_Count; } - private: - uint32_t m_RendererID; - uint32_t m_Count; - }; -}; \ No newline at end of file diff --git a/StarEngine/src/Platform/OpenGL/OpenGLContext.cpp b/StarEngine/src/Platform/OpenGL/OpenGLContext.cpp deleted file mode 100644 index 725f332c..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLContext.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "sepch.h" -#include "Platform/OpenGL/OpenGLContext.h" - -#include -#include - -namespace StarEngine { - - OpenGLContext::OpenGLContext(GLFWwindow* windowHandle) - : m_WindowHandle(windowHandle) - { - SE_CORE_ASSERT(windowHandle, "Window handle is null!") - } - - void OpenGLContext::Init() - { - SE_PROFILE_FUNCTION(); - - glfwMakeContextCurrent(m_WindowHandle); - int status = gladLoadGLLoader((GLADloadproc)glfwGetProcAddress); - SE_CORE_ASSERT(status, "Failed to initialize Glad!"); - - SE_CORE_INFO("OpenGL Info:"); - SE_CORE_INFO(" Vendor: {0}", (const char*)glGetString(GL_VENDOR)); - SE_CORE_INFO(" Renderer: {0}", (const char*)glGetString(GL_RENDERER)); - SE_CORE_INFO(" Version: {0}", (const char*)glGetString(GL_VERSION)); - - SE_CORE_ASSERT(GLVersion.major > 4 || (GLVersion.major == 4 && GLVersion.minor >= 5), "StarEngine requires at least OpenGL version 4.5!"); - } - - void OpenGLContext::SwapBuffers() - { - SE_PROFILE_FUNCTION(); - - glfwSwapBuffers(m_WindowHandle); - } -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLContext.h b/StarEngine/src/Platform/OpenGL/OpenGLContext.h deleted file mode 100644 index 8c084727..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLContext.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/GraphicsContext.h" - -struct GLFWwindow; - -namespace StarEngine { - - class OpenGLContext : public GraphicsContext - { - public: - OpenGLContext(GLFWwindow* windowHandle); - - virtual void Init() override; - virtual void SwapBuffers() override; - private: - GLFWwindow* m_WindowHandle; - }; - -} \ No newline at end of file diff --git a/StarEngine/src/Platform/OpenGL/OpenGLFramebuffer.cpp b/StarEngine/src/Platform/OpenGL/OpenGLFramebuffer.cpp deleted file mode 100644 index e497d986..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLFramebuffer.cpp +++ /dev/null @@ -1,224 +0,0 @@ -#include "sepch.h" -#include "Platform/OpenGL/OpenGLFramebuffer.h" - -#include - -namespace StarEngine -{ - static const uint32_t s_MaxFramebufferSize = 8192; - - namespace Utils { - - static GLenum TextureTarget(bool multisampled) - { - return multisampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D; - } - - static void CreateTextures(bool multisampled, uint32_t* outID, uint32_t count) - { - glCreateTextures(TextureTarget(multisampled), count, outID); - } - - static void BindTexture(bool multisampled, uint32_t id) - { - glBindTexture(TextureTarget(multisampled), id); - } - - static void AttachColorTexture(uint32_t id, int samples, GLenum internalFormat, GLenum format, uint32_t width, uint32_t height, int index) - { - bool multisampled = samples > 1; - if (multisampled) - { - glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, internalFormat, width, height, GL_FALSE); - } - else - { - glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, nullptr); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multisampled), id, 0); - } - - static void AttachDepthTexture(uint32_t id, int samples, GLenum format, GLenum attachmentType, uint32_t width, uint32_t height) - { - bool multisampled = samples > 1; - if (multisampled) - { - glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE); - } - else - { - glTexStorage2D(GL_TEXTURE_2D, 1, format, width, height); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - - glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentType, TextureTarget(multisampled), id, 0); - } - - static bool IsDepthFormat(FramebufferTextureFormat format) - { - switch (format) - { - case FramebufferTextureFormat::DEPTH24STENCIL8: - return true; - } - - return false; - } - - static GLenum StarEditorFBTextureFormatToGL(FramebufferTextureFormat format) - { - switch (format) - { - case FramebufferTextureFormat::RGBA8: return GL_RGBA8; - case FramebufferTextureFormat::RED_INTEGER: return GL_RED_INTEGER; - } - - SE_CORE_ASSERT(false); - return 0; - } - - } - - OpenGLFramebuffer::OpenGLFramebuffer(const FramebufferSpecification& spec) : m_Specification(spec) - { - for (auto spec : m_Specification.Attachments.Attachments) - { - if (!Utils::IsDepthFormat(spec.TextureFormat)) - m_ColorAttachmentSpecifications.emplace_back(spec); - else - m_DepthAttachmentSpecification = spec; - } - - Invalidate(); - } - - OpenGLFramebuffer::~OpenGLFramebuffer() - { - glDeleteFramebuffers(1, &m_RendererID); - glDeleteTextures(m_ColorAttachments.size(), m_ColorAttachments.data()); - glDeleteTextures(1, &m_DepthAttachment); - } - - void OpenGLFramebuffer::Invalidate() - { - if (m_RendererID) - { - glDeleteFramebuffers(1, &m_RendererID); - glDeleteTextures(m_ColorAttachments.size(), m_ColorAttachments.data()); - glDeleteTextures(1, &m_DepthAttachment); - - m_ColorAttachments.clear(); - m_DepthAttachment = 0; - } - - glCreateFramebuffers(1, &m_RendererID); - glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID); - - bool multisample = m_Specification.Samples > 1; - - // Attachments - if (m_ColorAttachmentSpecifications.size()) - { - m_ColorAttachments.resize(m_ColorAttachmentSpecifications.size()); - Utils::CreateTextures(multisample, m_ColorAttachments.data(), m_ColorAttachments.size()); - - for (size_t i = 0; i < m_ColorAttachments.size(); i++) - { - Utils::BindTexture(multisample, m_ColorAttachments[i]); - switch (m_ColorAttachmentSpecifications[i].TextureFormat) - { - case FramebufferTextureFormat::RGBA8: - Utils::AttachColorTexture(m_ColorAttachments[i], m_Specification.Samples, GL_RGBA8, GL_RGBA, m_Specification.Width, m_Specification.Height, i); - break; - case FramebufferTextureFormat::RED_INTEGER: - Utils::AttachColorTexture(m_ColorAttachments[i], m_Specification.Samples, GL_R32I, GL_RED_INTEGER, m_Specification.Width, m_Specification.Height, i); - } - } - } - - if (m_DepthAttachmentSpecification.TextureFormat != FramebufferTextureFormat::None) - { - Utils::CreateTextures(multisample, &m_DepthAttachment, 1); - Utils::BindTexture(multisample, m_DepthAttachment); - switch (m_DepthAttachmentSpecification.TextureFormat) - { - case FramebufferTextureFormat::DEPTH24STENCIL8: - Utils::AttachDepthTexture(m_DepthAttachment, m_Specification.Samples, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT, m_Specification.Width, m_Specification.Height); - break; - } - } - - if (m_ColorAttachments.size() > 1) - { - SE_CORE_ASSERT(m_ColorAttachments.size() <= 4); - GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 }; - glDrawBuffers(m_ColorAttachments.size(), buffers); - } - else if (m_ColorAttachments.empty()) - { - // Only depth-pass - glDrawBuffer(GL_NONE); - } - - SE_CORE_ASSERT(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Framebuffer is incomplete!"); - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - void OpenGLFramebuffer::Bind() - { - glBindFramebuffer(GL_FRAMEBUFFER, m_RendererID); - glViewport(0, 0, m_Specification.Width, m_Specification.Height); - } - - void OpenGLFramebuffer::Unbind() - { - glBindFramebuffer(GL_FRAMEBUFFER, 0); - } - - void OpenGLFramebuffer::Resize(uint32_t width, uint32_t height) - { - if (width == 0 || height == 0 || width > s_MaxFramebufferSize || height > s_MaxFramebufferSize) - { - SE_CORE_WARN("Attempted to resize framebuffer to {0}, {1}", width, height); - return; - } - - m_Specification.Width = width; - m_Specification.Height = height; - - Invalidate(); - } - - int OpenGLFramebuffer::ReadPixel(uint32_t attachmentIndex, int x, int y) - { - SE_CORE_ASSERT(attachmentIndex < m_ColorAttachments.size()); - - glReadBuffer(GL_COLOR_ATTACHMENT0 + attachmentIndex); - int pixelData; - glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixelData); - return pixelData; - } - - void OpenGLFramebuffer::ClearAttachment(uint32_t attachmentIndex, int value) - { - SE_CORE_ASSERT(attachmentIndex < m_ColorAttachments.size()); - - auto& spec = m_ColorAttachmentSpecifications[attachmentIndex]; - glClearTexImage(m_ColorAttachments[attachmentIndex], 0, - Utils::StarEditorFBTextureFormatToGL(spec.TextureFormat), GL_INT, &value); - } - -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLFramebuffer.h b/StarEngine/src/Platform/OpenGL/OpenGLFramebuffer.h deleted file mode 100644 index 5d381fbb..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLFramebuffer.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/Framebuffer.h" - -namespace StarEngine { - class OpenGLFramebuffer : public Framebuffer - { - public: - OpenGLFramebuffer(const FramebufferSpecification& spec); - virtual ~OpenGLFramebuffer(); - - void Invalidate(); - - virtual void Bind() override; - virtual void Unbind() override; - - virtual void Resize(uint32_t width, uint32_t height) override; - virtual int ReadPixel(uint32_t attachmentIndex, int x, int y) override; - - virtual void ClearAttachment(uint32_t attachmentIndex, int value) override; - - virtual uint32_t GetColorAttachmentRendererID(uint32_t index = 0) const override { SE_CORE_ASSERT(index < m_ColorAttachments.size()); return m_ColorAttachments[index]; } - - virtual const FramebufferSpecification& GetSpecification() const override { return m_Specification; } - private: - uint32_t m_RendererID = 0; - FramebufferSpecification m_Specification; - - std::vector m_ColorAttachmentSpecifications; - FramebufferTextureSpecification m_DepthAttachmentSpecification = FramebufferTextureFormat::None; - - std::vector m_ColorAttachments; - uint32_t m_DepthAttachment = 0; - }; -} - - diff --git a/StarEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp b/StarEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp deleted file mode 100644 index cbaec12a..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLRendererAPI.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "sepch.h" -#include "Platform/OpenGL/OpenGLRendererAPI.h" - -#include - -namespace StarEngine -{ - void OpenGLMessageCallback( - unsigned source, - unsigned type, - unsigned id, - unsigned severity, - int length, - const char* message, - const void* userParam) - { - switch (severity) - { - case GL_DEBUG_SEVERITY_HIGH: SE_CORE_CRITICAL(message); return; - case GL_DEBUG_SEVERITY_MEDIUM: SE_CORE_ERROR(message); return; - case GL_DEBUG_SEVERITY_LOW: SE_CORE_WARN(message); return; - case GL_DEBUG_SEVERITY_NOTIFICATION: SE_CORE_TRACE(message); return; - } - - SE_CORE_ASSERT(false, "Unknown severity level!"); - } - - void OpenGLRendererAPI::Init() - { - SE_PROFILE_FUNCTION(); - - #ifdef SE_DEBUG - glEnable(GL_DEBUG_OUTPUT); - glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); - glDebugMessageCallback(OpenGLMessageCallback, nullptr); - - glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION, 0, NULL, GL_FALSE); - #endif - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_DEPTH_TEST); - - glEnable(GL_LINE_SMOOTH); - } - - void OpenGLRendererAPI::SetViewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) - { - glViewport(x, y, width, height); - } - - void OpenGLRendererAPI::SetClearColor(const glm::vec4& color) - { - glClearColor(color.r, color.g, color.b, color.a); - } - void OpenGLRendererAPI::Clear() - { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } - void OpenGLRendererAPI::DrawIndexed(const Ref& vertexArray, uint32_t indexCount) - { - vertexArray->Bind(); - uint32_t count = indexCount ? indexCount : vertexArray->GetIndexBuffer()->GetCount(); - glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, nullptr); - } - - void OpenGLRendererAPI::DrawLines(const Ref& vertexArray, uint32_t vertexCount) - { - vertexArray->Bind(); - glDrawArrays(GL_LINES, 0, vertexCount); - } - - void OpenGLRendererAPI::SetLineWidth(float width) - { - glLineWidth(width); - } -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLRendererAPI.h b/StarEngine/src/Platform/OpenGL/OpenGLRendererAPI.h deleted file mode 100644 index 918cb090..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLRendererAPI.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/RendererAPI.h" - -namespace StarEngine -{ - class OpenGLRendererAPI : public RendererAPI - { - public: - virtual void Init() override; - virtual void SetViewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) override; - - virtual void SetClearColor(const glm::vec4& color) override; - virtual void Clear() override; - - virtual void DrawIndexed(const Ref& vertexArray, uint32_t indexCount = 0) override; - virtual void DrawLines(const Ref& vertexArray, uint32_t vertexCount) override; - - virtual void SetLineWidth(float width) override; - }; -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLShader.cpp b/StarEngine/src/Platform/OpenGL/OpenGLShader.cpp deleted file mode 100644 index 99a56f63..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLShader.cpp +++ /dev/null @@ -1,480 +0,0 @@ -#include "sepch.h" -#include "Platform/OpenGL/OpenGLShader.h" -#include "StarEngine/Core/Timer.h" - -#include -#include -#include - -#include -#include -#include - -namespace StarEngine { - - namespace Utils { - - static GLenum ShaderTypeFromString(const std::string& type) - { - if (type == "vertex") - return GL_VERTEX_SHADER; - if (type == "fragment" || type == "pixel") - return GL_FRAGMENT_SHADER; - - SE_CORE_ASSERT(false, "Unknown shader type!"); - return 0; - } - - static shaderc_shader_kind GLShaderStageToShaderC(GLenum stage) - { - switch (stage) - { - case GL_VERTEX_SHADER: return shaderc_glsl_vertex_shader; - case GL_FRAGMENT_SHADER: return shaderc_glsl_fragment_shader; - } - SE_CORE_ASSERT(false); - return (shaderc_shader_kind)0; - } - - static const char* GLShaderStageToString(GLenum stage) - { - switch (stage) - { - case GL_VERTEX_SHADER: return "GL_VERTEX_SHADER"; - case GL_FRAGMENT_SHADER: return "GL_FRAGMENT_SHADER"; - } - SE_CORE_ASSERT(false); - return nullptr; - } - - static const char* GetCacheDirectory() - { - // TODO: make sure the assets directory is valid - return "assets/cache/shader/opengl"; - } - - static void CreateCacheDirectoryIfNeeded() - { - std::string cacheDirectory = GetCacheDirectory(); - if (!std::filesystem::exists(cacheDirectory)) - std::filesystem::create_directories(cacheDirectory); - } - - static const char* GLShaderStageCachedOpenGLFileExtension(uint32_t stage) - { - switch (stage) - { - case GL_VERTEX_SHADER: return ".cached_opengl.vert"; - case GL_FRAGMENT_SHADER: return ".cached_opengl.frag"; - } - SE_CORE_ASSERT(false); - return ""; - } - - static const char* GLShaderStageCachedVulkanFileExtension(uint32_t stage) - { - switch (stage) - { - case GL_VERTEX_SHADER: return ".cached_vulkan.vert"; - case GL_FRAGMENT_SHADER: return ".cached_vulkan.frag"; - } - SE_CORE_ASSERT(false); - return ""; - } - - - } - - OpenGLShader::OpenGLShader(const std::string& filepath) - : m_FilePath(filepath) - { - SE_PROFILE_FUNCTION(); - - Utils::CreateCacheDirectoryIfNeeded(); - - std::string source = ReadFile(filepath); - auto shaderSources = PreProcess(source); - - { - Timer timer; - CompileOrGetVulkanBinaries(shaderSources); - CompileOrGetOpenGLBinaries(); - CreateProgram(); - SE_CORE_WARN("Shader creation took {0} ms", timer.ElapsedMillis()); - } - - // Extract name from filepath - auto lastSlash = filepath.find_last_of("/\\"); - lastSlash = lastSlash == std::string::npos ? 0 : lastSlash + 1; - auto lastDot = filepath.rfind('.'); - auto count = lastDot == std::string::npos ? filepath.size() - lastSlash : lastDot - lastSlash; - m_Name = filepath.substr(lastSlash, count); - } - - OpenGLShader::OpenGLShader(const std::string& name, const std::string& vertexSrc, const std::string& fragmentSrc) - : m_Name(name) - { - SE_PROFILE_FUNCTION(); - - std::unordered_map sources; - sources[GL_VERTEX_SHADER] = vertexSrc; - sources[GL_FRAGMENT_SHADER] = fragmentSrc; - - CompileOrGetVulkanBinaries(sources); - CompileOrGetOpenGLBinaries(); - CreateProgram(); - } - - OpenGLShader::~OpenGLShader() - { - SE_PROFILE_FUNCTION(); - - glDeleteProgram(m_RendererID); - } - - std::string OpenGLShader::ReadFile(const std::string& filepath) - { - SE_PROFILE_FUNCTION(); - - std::string result; - std::ifstream in(filepath, std::ios::in | std::ios::binary); // ifstream closes itself due to RAII - if (in) - { - in.seekg(0, std::ios::end); - size_t size = in.tellg(); - if (size != -1) - { - result.resize(size); - in.seekg(0, std::ios::beg); - in.read(&result[0], size); - } - else - { - SE_CORE_ERROR("Could not read from file '{0}'", filepath); - } - } - else - { - SE_CORE_ERROR("Could not open file '{0}'", filepath); - } - - return result; - } - - std::unordered_map OpenGLShader::PreProcess(const std::string& source) - { - SE_PROFILE_FUNCTION(); - - std::unordered_map shaderSources; - - const char* typeToken = "#type"; - size_t typeTokenLength = strlen(typeToken); - size_t pos = source.find(typeToken, 0); //Start of shader type declaration line - while (pos != std::string::npos) - { - size_t eol = source.find_first_of("\r\n", pos); //End of shader type declaration line - SE_CORE_ASSERT(eol != std::string::npos, "Syntax error"); - size_t begin = pos + typeTokenLength + 1; //Start of shader type name (after "#type " keyword) - std::string type = source.substr(begin, eol - begin); - SE_CORE_ASSERT(Utils::ShaderTypeFromString(type), "Invalid shader type specified"); - - size_t nextLinePos = source.find_first_not_of("\r\n", eol); //Start of shader code after shader type declaration line - SE_CORE_ASSERT(nextLinePos != std::string::npos, "Syntax error"); - pos = source.find(typeToken, nextLinePos); //Start of next shader type declaration line - - shaderSources[Utils::ShaderTypeFromString(type)] = (pos == std::string::npos) ? source.substr(nextLinePos) : source.substr(nextLinePos, pos - nextLinePos); - } - - return shaderSources; - } - - void OpenGLShader::CompileOrGetVulkanBinaries(const std::unordered_map& shaderSources) - { - GLuint program = glCreateProgram(); - - shaderc::Compiler compiler; - shaderc::CompileOptions options; - options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); - const bool optimize = false; - if (optimize) - options.SetOptimizationLevel(shaderc_optimization_level_performance); - - std::filesystem::path cacheDirectory = Utils::GetCacheDirectory(); - - auto& shaderData = m_VulkanSPIRV; - shaderData.clear(); - for (auto&& [stage, source] : shaderSources) - { - std::filesystem::path shaderFilePath = m_FilePath; - std::filesystem::path cachedPath = cacheDirectory / (shaderFilePath.filename().string() + Utils::GLShaderStageCachedVulkanFileExtension(stage)); - - std::ifstream in(cachedPath, std::ios::in | std::ios::binary); - if (in.is_open()) - { - in.seekg(0, std::ios::end); - auto size = in.tellg(); - in.seekg(0, std::ios::beg); - - auto& data = shaderData[stage]; - data.resize(size / sizeof(uint32_t)); - in.read((char*)data.data(), size); - } - else - { - shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, Utils::GLShaderStageToShaderC(stage), m_FilePath.c_str(), options); - if (module.GetCompilationStatus() != shaderc_compilation_status_success) - { - SE_CORE_ERROR(module.GetErrorMessage()); - SE_CORE_ASSERT(false); - } - - shaderData[stage] = std::vector(module.cbegin(), module.cend()); - - std::ofstream out(cachedPath, std::ios::out | std::ios::binary); - if (out.is_open()) - { - auto& data = shaderData[stage]; - out.write((char*)data.data(), data.size() * sizeof(uint32_t)); - out.flush(); - out.close(); - } - } - } - - for (auto&& [stage, data] : shaderData) - Reflect(stage, data); - } - - void OpenGLShader::CompileOrGetOpenGLBinaries() - { - auto& shaderData = m_OpenGLSPIRV; - - shaderc::Compiler compiler; - shaderc::CompileOptions options; - options.SetTargetEnvironment(shaderc_target_env_opengl, shaderc_env_version_opengl_4_5); - const bool optimize = false; - if (optimize) - options.SetOptimizationLevel(shaderc_optimization_level_performance); - - std::filesystem::path cacheDirectory = Utils::GetCacheDirectory(); - - shaderData.clear(); - m_OpenGLSourceCode.clear(); - for (auto&& [stage, spirv] : m_VulkanSPIRV) - { - std::filesystem::path shaderFilePath = m_FilePath; - std::filesystem::path cachedPath = cacheDirectory / (shaderFilePath.filename().string() + Utils::GLShaderStageCachedOpenGLFileExtension(stage)); - - std::ifstream in(cachedPath, std::ios::in | std::ios::binary); - if (in.is_open()) - { - in.seekg(0, std::ios::end); - auto size = in.tellg(); - in.seekg(0, std::ios::beg); - - auto& data = shaderData[stage]; - data.resize(size / sizeof(uint32_t)); - in.read((char*)data.data(), size); - } - else - { - spirv_cross::CompilerGLSL glslCompiler(spirv); - m_OpenGLSourceCode[stage] = glslCompiler.compile(); - auto& source = m_OpenGLSourceCode[stage]; - - shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, Utils::GLShaderStageToShaderC(stage), m_FilePath.c_str()); - if (module.GetCompilationStatus() != shaderc_compilation_status_success) - { - SE_CORE_ERROR(module.GetErrorMessage()); - SE_CORE_ASSERT(false); - } - - shaderData[stage] = std::vector(module.cbegin(), module.cend()); - - std::ofstream out(cachedPath, std::ios::out | std::ios::binary); - if (out.is_open()) - { - auto& data = shaderData[stage]; - out.write((char*)data.data(), data.size() * sizeof(uint32_t)); - out.flush(); - out.close(); - } - } - } - } - - void OpenGLShader::CreateProgram() - { - GLuint program = glCreateProgram(); - - std::vector shaderIDs; - for (auto&& [stage, spirv] : m_OpenGLSPIRV) - { - GLuint shaderID = shaderIDs.emplace_back(glCreateShader(stage)); - glShaderBinary(1, &shaderID, GL_SHADER_BINARY_FORMAT_SPIR_V, spirv.data(), spirv.size() * sizeof(uint32_t)); - glSpecializeShader(shaderID, "main", 0, nullptr, nullptr); - glAttachShader(program, shaderID); - } - - glLinkProgram(program); - - GLint isLinked; - glGetProgramiv(program, GL_LINK_STATUS, &isLinked); - if (isLinked == GL_FALSE) - { - GLint maxLength; - glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength); - - std::vector infoLog(maxLength); - glGetProgramInfoLog(program, maxLength, &maxLength, infoLog.data()); - SE_CORE_ERROR("Shader linking failed ({0}):\n{1}", m_FilePath, infoLog.data()); - - glDeleteProgram(program); - - for (auto id : shaderIDs) - glDeleteShader(id); - } - - for (auto id : shaderIDs) - { - glDetachShader(program, id); - glDeleteShader(id); - } - - m_RendererID = program; - } - - void OpenGLShader::Reflect(GLenum stage, const std::vector& shaderData) - { - spirv_cross::Compiler compiler(shaderData); - spirv_cross::ShaderResources resources = compiler.get_shader_resources(); - - SE_CORE_TRACE("OpenGLShader::Reflect - {0} {1}", Utils::GLShaderStageToString(stage), m_FilePath); - SE_CORE_TRACE(" {0} uniform buffers", resources.uniform_buffers.size()); - SE_CORE_TRACE(" {0} resources", resources.sampled_images.size()); - - SE_CORE_TRACE("Uniform buffers:"); - for (const auto& resource : resources.uniform_buffers) - { - const auto& bufferType = compiler.get_type(resource.base_type_id); - uint32_t bufferSize = compiler.get_declared_struct_size(bufferType); - uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding); - int memberCount = bufferType.member_types.size(); - - SE_CORE_TRACE(" {0}", resource.name); - SE_CORE_TRACE(" Size = {0}", bufferSize); - SE_CORE_TRACE(" Binding = {0}", binding); - SE_CORE_TRACE(" Members = {0}", memberCount); - } - } - - void OpenGLShader::Bind() const - { - SE_PROFILE_FUNCTION(); - - glUseProgram(m_RendererID); - } - - void OpenGLShader::Unbind() const - { - SE_PROFILE_FUNCTION(); - - glUseProgram(0); - } - - void OpenGLShader::SetInt(const std::string& name, int value) - { - SE_PROFILE_FUNCTION(); - - UploadUniformInt(name, value); - } - - void OpenGLShader::SetIntArray(const std::string& name, int* values, uint32_t count) - { - UploadUniformIntArray(name, values, count); - } - - void OpenGLShader::SetFloat(const std::string& name, float value) - { - SE_PROFILE_FUNCTION(); - - UploadUniformFloat(name, value); - } - - void OpenGLShader::SetFloat2(const std::string& name, const glm::vec2& value) - { - SE_PROFILE_FUNCTION(); - - UploadUniformFloat2(name, value); - } - - void OpenGLShader::SetFloat3(const std::string& name, const glm::vec3& value) - { - SE_PROFILE_FUNCTION(); - - UploadUniformFloat3(name, value); - } - - void OpenGLShader::SetFloat4(const std::string& name, const glm::vec4& value) - { - SE_PROFILE_FUNCTION(); - - UploadUniformFloat4(name, value); - } - - void OpenGLShader::SetMat4(const std::string& name, const glm::mat4& value) - { - SE_PROFILE_FUNCTION(); - - UploadUniformMat4(name, value); - } - - void OpenGLShader::UploadUniformInt(const std::string& name, int value) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniform1i(location, value); - } - - void OpenGLShader::UploadUniformIntArray(const std::string& name, int* values, uint32_t count) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniform1iv(location, count, values); - } - - void OpenGLShader::UploadUniformFloat(const std::string& name, float value) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniform1f(location, value); - } - - void OpenGLShader::UploadUniformFloat2(const std::string& name, const glm::vec2& value) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniform2f(location, value.x, value.y); - } - - void OpenGLShader::UploadUniformFloat3(const std::string& name, const glm::vec3& value) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniform3f(location, value.x, value.y, value.z); - } - - void OpenGLShader::UploadUniformFloat4(const std::string& name, const glm::vec4& value) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniform4f(location, value.x, value.y, value.z, value.w); - } - - void OpenGLShader::UploadUniformMat3(const std::string& name, const glm::mat3& matrix) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniformMatrix3fv(location, 1, GL_FALSE, glm::value_ptr(matrix)); - } - - void OpenGLShader::UploadUniformMat4(const std::string& name, const glm::mat4& matrix) - { - GLint location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix)); - } - -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLShader.h b/StarEngine/src/Platform/OpenGL/OpenGLShader.h deleted file mode 100644 index afc0f396..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLShader.h +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/Shader.h" - -#include - -//TODO REMOVE THIS -typedef unsigned int GLenum; - -namespace StarEngine { - - class OpenGLShader : public StarEngine::Shader - { - - public: - - OpenGLShader(const std::string& filepath); - OpenGLShader(const std::string& name, const std::string& vertexSrc, const std::string& fragmentSrc); - - virtual ~OpenGLShader(); - - virtual void Bind() const override; - virtual void Unbind() const override; - - virtual void SetInt(const std::string& name, int value) override; - virtual void SetIntArray(const std::string& name, int* values, uint32_t count) override; - virtual void SetFloat(const std::string& name, float value) override; - virtual void SetFloat2(const std::string& name, const glm::vec2& value) override; - virtual void SetFloat3(const std::string& name, const glm::vec3& value) override; - virtual void SetFloat4(const std::string& name, const glm::vec4& value) override; - virtual void SetMat4(const std::string& name, const glm::mat4& value) override; - - virtual const std::string& GetName() const override { return m_Name; } - - void UploadUniformInt(const std::string& name, int value); - void UploadUniformIntArray(const std::string& name, int* values, uint32_t count); - - void UploadUniformFloat(const std::string& name, float value); - void UploadUniformFloat2(const std::string& name, const glm::vec2& value); - void UploadUniformFloat3(const std::string& name, const glm::vec3& value); - void UploadUniformFloat4(const std::string& name, const glm::vec4& value); - - void UploadUniformMat3(const std::string& name, const glm::mat3& matrix); - void UploadUniformMat4(const std::string& name, const glm::mat4& matrix); - - private: - std::string ReadFile(const std::string& filepath); - std::unordered_map PreProcess(const std::string& source); - - void CompileOrGetVulkanBinaries(const std::unordered_map& shaderSources); - void CompileOrGetOpenGLBinaries(); - void CreateProgram(); - void Reflect(GLenum stage, const std::vector& shaderData); - private: - uint32_t m_RendererID; - std::string m_FilePath; - std::string m_Name; - - std::unordered_map> m_VulkanSPIRV; - std::unordered_map> m_OpenGLSPIRV; - - std::unordered_map m_OpenGLSourceCode; - }; -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLTexture.cpp b/StarEngine/src/Platform/OpenGL/OpenGLTexture.cpp deleted file mode 100644 index 15e8b580..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLTexture.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "sepch.h" -#include "Platform/OpenGL/OpenGLTexture.h" - -#include "stb_image.h" - -namespace StarEngine { - - namespace Utils { - - static GLenum StarEngineImageFormatToGLDataFormat(ImageFormat format) - { - switch (format) - { - case ImageFormat::RGB8: return GL_RGB; - case ImageFormat::RGBA8: return GL_RGBA; - } - - SE_CORE_ASSERT(false); - return 0; - } - - static GLenum StarEngineImageFormatToGLInternalFormat(ImageFormat format) - { - switch (format) - { - case ImageFormat::RGB8: return GL_RGB8; - case ImageFormat::RGBA8: return GL_RGBA8; - } - - SE_CORE_ASSERT(false); - return 0; - } - - } - - OpenGLTexture2D::OpenGLTexture2D(const TextureSpecification& specification, Buffer data) - : m_Specification(specification), m_Width(m_Specification.Width), m_Height(m_Specification.Height) - { - SE_PROFILE_FUNCTION(); - - m_InternalFormat = Utils::StarEngineImageFormatToGLInternalFormat(m_Specification.Format); - m_DataFormat = Utils::StarEngineImageFormatToGLDataFormat(m_Specification.Format); - - glCreateTextures(GL_TEXTURE_2D, 1, &m_RendererID); - glTextureStorage2D(m_RendererID, 1, m_InternalFormat, m_Width, m_Height); - - glTextureParameteri(m_RendererID, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTextureParameteri(m_RendererID, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - glTextureParameteri(m_RendererID, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTextureParameteri(m_RendererID, GL_TEXTURE_WRAP_T, GL_REPEAT); - - if (data) - SetData(data); - } - - OpenGLTexture2D::~OpenGLTexture2D() - { - SE_PROFILE_FUNCTION(); - - glDeleteTextures(1, &m_RendererID); - } - - void OpenGLTexture2D::ChangeSize(uint32_t newWidth, uint32_t newHeight) - { - //Create new texture - uint32_t newTextureID; - glCreateTextures(GL_TEXTURE_2D, 1, &newTextureID); - glTextureStorage2D(newTextureID, 1, m_InternalFormat, newWidth, newHeight); - - glTextureParameteri(newTextureID, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTextureParameteri(newTextureID, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - glTextureParameteri(newTextureID, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTextureParameteri(newTextureID, GL_TEXTURE_WRAP_T, GL_REPEAT); - - GLuint framebufferRendererIDs[2] = { 0 }; - glGenFramebuffers(2, framebufferRendererIDs); - - glBindFramebuffer(GL_READ_FRAMEBUFFER, framebufferRendererIDs[0]); - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_RendererID, 0); - - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebufferRendererIDs[1]); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, newTextureID, 0); - - glBlitFramebuffer(0, 0, m_Width, m_Height, 0, 0, newWidth, newHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); - - glDeleteTextures(1, &m_RendererID); - glDeleteFramebuffers(2, framebufferRendererIDs); - - m_RendererID = newTextureID; - m_Width = newWidth; - m_Height = newHeight; - } - - void OpenGLTexture2D::SetData(Buffer data) - { - SE_PROFILE_FUNCTION(); - - uint32_t bpp = m_DataFormat == GL_RGBA ? 4 : 3; - SE_CORE_ASSERT(data.Size == m_Width * m_Height * bpp, "Data must be entire texture!"); - glTextureSubImage2D(m_RendererID, 0, 0, 0, m_Width, m_Height, m_DataFormat, GL_UNSIGNED_BYTE, data.Data); - } - - void OpenGLTexture2D::Bind(uint32_t slot) const - { - SE_PROFILE_FUNCTION(); - - glBindTextureUnit(slot, m_RendererID); - } -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLTexture.h b/StarEngine/src/Platform/OpenGL/OpenGLTexture.h deleted file mode 100644 index 160b8781..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLTexture.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/Texture.h" - -#include - -namespace StarEngine { - - class OpenGLTexture2D : public Texture2D - { - public: - OpenGLTexture2D(const TextureSpecification& specification, Buffer data = Buffer()); - virtual ~OpenGLTexture2D(); - - virtual const TextureSpecification& GetSpecification() const override { return m_Specification; } - - virtual uint32_t GetWidth() const override { return m_Width; } - virtual uint32_t GetHeight() const override { return m_Height; } - virtual uint32_t GetRendererID() const override { return m_RendererID; } - - virtual uint64_t GetEstimatedSize() const override { return m_Width * m_Height * 4; } // Assuming RGBA8 format - - virtual void ChangeSize(uint32_t newWidth, uint32_t newHeight) override; - - virtual void SetData(Buffer data) override; - - virtual void Bind(uint32_t slot = 0) const override; - - virtual bool IsLoaded() const override { return m_IsLoaded; } - - virtual bool operator==(const Texture& other) const override - { - return m_RendererID == other.GetRendererID(); - } - private: - TextureSpecification m_Specification; - - bool m_IsLoaded = false; - uint32_t m_Width, m_Height; - uint32_t m_RendererID; - GLenum m_InternalFormat, m_DataFormat; - }; - -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLUniformBuffer.cpp b/StarEngine/src/Platform/OpenGL/OpenGLUniformBuffer.cpp deleted file mode 100644 index d160825c..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLUniformBuffer.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "sepch.h" -#include "OpenGLUniformBuffer.h" - -#include - -namespace StarEngine { - - OpenGLUniformBuffer::OpenGLUniformBuffer(uint32_t size, uint32_t binding) - { - glCreateBuffers(1, &m_RendererID); - glNamedBufferData(m_RendererID, size, nullptr, GL_DYNAMIC_DRAW); // TODO: investigate usage hint - glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_RendererID); - } - - OpenGLUniformBuffer::~OpenGLUniformBuffer() - { - glDeleteBuffers(1, &m_RendererID); - } - - - void OpenGLUniformBuffer::SetData(const void* data, uint32_t size, uint32_t offset) - { - glNamedBufferSubData(m_RendererID, offset, size, data); - } - -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLUniformBuffer.h b/StarEngine/src/Platform/OpenGL/OpenGLUniformBuffer.h deleted file mode 100644 index af0bd0ca..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLUniformBuffer.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/UniformBuffer.h" - -namespace StarEngine { - - class OpenGLUniformBuffer : public UniformBuffer - { - public: - OpenGLUniformBuffer(uint32_t size, uint32_t binding); - virtual ~OpenGLUniformBuffer(); - - virtual void SetData(const void* data, uint32_t size, uint32_t offset = 0) override; - private: - uint32_t m_RendererID = 0; - }; -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLVertexArray.cpp b/StarEngine/src/Platform/OpenGL/OpenGLVertexArray.cpp deleted file mode 100644 index 283fdc75..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLVertexArray.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "sepch.h" -#include "Platform/OpenGL/OpenGLVertexArray.h" - -#include - -namespace StarEngine { - - static GLenum ShaderDataTypeToOpenGLBaseType(ShaderDataType type) - { - switch (type) - { - case ShaderDataType::Float: return GL_FLOAT; - case ShaderDataType::Float2: return GL_FLOAT; - case ShaderDataType::Float3: return GL_FLOAT; - case ShaderDataType::Float4: return GL_FLOAT; - case ShaderDataType::Mat3: return GL_FLOAT; - case ShaderDataType::Mat4: return GL_FLOAT; - case ShaderDataType::Int: return GL_INT; - case ShaderDataType::Int2: return GL_INT; - case ShaderDataType::Int3: return GL_INT; - case ShaderDataType::Int4: return GL_INT; - case ShaderDataType::Bool: return GL_BOOL; - } - - SE_CORE_ASSERT(false, "Unknown ShaderDataType!"); - return 0; - } - - OpenGLVertexArray::OpenGLVertexArray() - { - SE_PROFILE_FUNCTION(); - - glCreateVertexArrays(1, &m_RendererID); - } - - OpenGLVertexArray::~OpenGLVertexArray() - { - SE_PROFILE_FUNCTION(); - - glDeleteVertexArrays(1, &m_RendererID); - } - - void OpenGLVertexArray::Bind() const - { - SE_PROFILE_FUNCTION(); - - glBindVertexArray(m_RendererID); - } - - void OpenGLVertexArray::Unbind() const - { - SE_PROFILE_FUNCTION(); - - glBindVertexArray(0); - } - - void OpenGLVertexArray::AddVertexBuffer(const Ref& vertexBuffer) - { - SE_PROFILE_FUNCTION(); - - SE_CORE_ASSERT(vertexBuffer->GetLayout().GetElements().size(), "Vertex Buffer has no layout!"); - - glBindVertexArray(m_RendererID); - vertexBuffer->Bind(); - - const auto& layout = vertexBuffer->GetLayout(); - for (const auto& element : layout) - { - switch (element.Type) - { - case ShaderDataType::Float: - case ShaderDataType::Float2: - case ShaderDataType::Float3: - case ShaderDataType::Float4: - { - glEnableVertexAttribArray(m_VertexBufferIndex); - glVertexAttribPointer(m_VertexBufferIndex, - element.GetComponentCount(), - ShaderDataTypeToOpenGLBaseType(element.Type), - element.Normalized ? GL_TRUE : GL_FALSE, - layout.GetStride(), - (const void*)element.Offset); - m_VertexBufferIndex++; - break; - } - case ShaderDataType::Int: - case ShaderDataType::Int2: - case ShaderDataType::Int3: - case ShaderDataType::Int4: - case ShaderDataType::Bool: - { - glEnableVertexAttribArray(m_VertexBufferIndex); - glVertexAttribIPointer(m_VertexBufferIndex, - element.GetComponentCount(), - ShaderDataTypeToOpenGLBaseType(element.Type), - layout.GetStride(), - (const void*)element.Offset); - m_VertexBufferIndex++; - break; - } - case ShaderDataType::Mat3: - case ShaderDataType::Mat4: - { - uint8_t count = element.GetComponentCount(); - for (uint8_t i = 0; i < count; i++) - { - glEnableVertexAttribArray(m_VertexBufferIndex); - glVertexAttribPointer(m_VertexBufferIndex, - count, - ShaderDataTypeToOpenGLBaseType(element.Type), - element.Normalized ? GL_TRUE : GL_FALSE, - layout.GetStride(), - (const void*)(element.Offset + sizeof(float) * count * i)); - glVertexAttribDivisor(m_VertexBufferIndex, 1); - m_VertexBufferIndex++; - } - break; - } - default: - SE_CORE_ASSERT(false, "Unknown ShaderDataType!"); - } - } - - m_VertexBuffers.push_back(vertexBuffer); - } - - void OpenGLVertexArray::SetIndexBuffer(const Ref& indexBuffer) - { - SE_PROFILE_FUNCTION(); - - glBindVertexArray(m_RendererID); - indexBuffer->Bind(); - - m_IndexBuffer = indexBuffer; - } - -} diff --git a/StarEngine/src/Platform/OpenGL/OpenGLVertexArray.h b/StarEngine/src/Platform/OpenGL/OpenGLVertexArray.h deleted file mode 100644 index d6c81b1c..00000000 --- a/StarEngine/src/Platform/OpenGL/OpenGLVertexArray.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "StarEngine/Renderer/VertexArray.h" - -namespace StarEngine { - - class OpenGLVertexArray : public VertexArray - { - public: - OpenGLVertexArray(); - virtual ~OpenGLVertexArray(); - - virtual void Bind() const override; - virtual void Unbind() const override; - - virtual void AddVertexBuffer(const Ref& vertexBuffer) override; - virtual void SetIndexBuffer(const Ref& indexBuffer) override; - - virtual const std::vector>& GetVertexBuffers() const override { return m_VertexBuffers; } - virtual const Ref& GetIndexBuffer() const override { return m_IndexBuffer; } - - private: - uint32_t m_RendererID; - uint32_t m_VertexBufferIndex = 0; - std::vector> m_VertexBuffers; - Ref m_IndexBuffer; - }; -} diff --git a/StarEngine/src/Platform/Windows/WindowsInput.cpp b/StarEngine/src/Platform/Windows/WindowsInput.cpp deleted file mode 100644 index 51d984d0..00000000 --- a/StarEngine/src/Platform/Windows/WindowsInput.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "sepch.h" -#include "StarEngine/Core/Input.h" -#include "StarEngine/Core/Application.h" - -#include - -namespace StarEngine { - - bool Input::IsKeyPressed(const KeyCode key) - { - auto window = static_cast(Application::Get().GetWindow().GetNativeWindow()); - auto state = glfwGetKey(window, static_cast(key)); - return state == GLFW_PRESS; - } - - bool Input::IsMouseButtonPressed(const MouseCode button) - { - auto window = static_cast(Application::Get().GetWindow().GetNativeWindow()); - auto state = glfwGetMouseButton(window, static_cast(button)); - return state == GLFW_PRESS; - } - - glm::vec2 Input::GetMousePosition() - { - auto* window = static_cast(Application::Get().GetWindow().GetNativeWindow()); - double xpos, ypos; - glfwGetCursorPos(window, &xpos, &ypos); - - return { (float)xpos, (float)ypos }; - } - - float Input::GetMouseX() - { - return GetMousePosition().x; - } - - float Input::GetMouseY() - { - return GetMousePosition().y; - } -} diff --git a/StarEngine/src/Platform/Windows/WindowsPlatformUtils.cpp b/StarEngine/src/Platform/Windows/WindowsPlatformUtils.cpp deleted file mode 100644 index 270500b0..00000000 --- a/StarEngine/src/Platform/Windows/WindowsPlatformUtils.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "sepch.h" -#include "StarEngine/Utils/PlatformUtils.h" - -#include "StarEngine/Core/Application.h" - -#include -#include -#define GLFW_EXPOSE_NATIVE_WIN32 -#include - -namespace StarEngine { - - float Time::GetTime() - { - return glfwGetTime(); - } - - std::string FileDialogs::OpenFile(const char* filter) - { - OPENFILENAMEA ofn; - CHAR szFile[260] = { 0 }; - CHAR currentDir[256] = { 0 }; - ZeroMemory(&ofn, sizeof(OPENFILENAME)); - ofn.lStructSize = sizeof(OPENFILENAME); - ofn.hwndOwner = glfwGetWin32Window((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow()); - ofn.lpstrFile = szFile; - ofn.nMaxFile = sizeof(szFile); - if (GetCurrentDirectoryA(256, currentDir)) - ofn.lpstrInitialDir = currentDir; - ofn.lpstrFilter = filter; - ofn.nFilterIndex = 1; - ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; - - if (GetOpenFileNameA(&ofn) == TRUE) - { - return ofn.lpstrFile; - } - - return std::string(); - } - - std::string FileDialogs::SaveFile(const char* filter) - { - OPENFILENAMEA ofn; - CHAR szFile[260] = { 0 }; - CHAR currentDir[256] = { 0 }; - ZeroMemory(&ofn, sizeof(OPENFILENAME)); - ofn.lStructSize = sizeof(OPENFILENAME); - ofn.hwndOwner = glfwGetWin32Window((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow()); - ofn.lpstrFile = szFile; - ofn.nMaxFile = sizeof(szFile); - if (GetCurrentDirectoryA(256, currentDir)) - ofn.lpstrInitialDir = currentDir; - ofn.lpstrFilter = filter; - ofn.nFilterIndex = 1; - ofn.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR; - ofn.lpstrDefExt = strchr(filter, '\0') + 1; // Sets the default extension by extracting it from the filter - - if (GetSaveFileNameA(&ofn) == TRUE) - { - return ofn.lpstrFile; - } - - return std::string(); - } - -} diff --git a/StarEngine/src/Platform/Windows/WindowsWindow.cpp b/StarEngine/src/Platform/Windows/WindowsWindow.cpp deleted file mode 100644 index 9ac4ee61..00000000 --- a/StarEngine/src/Platform/Windows/WindowsWindow.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "sepch.h" -#include "Platform/Windows/WindowsWindow.h" - -#include "StarEngine/Core/Input.h" - -#include "StarEngine/Events/ApplicationEvent.h" -#include "StarEngine/Events/MouseEvent.h" -#include "StarEngine/Events/KeyEvent.h" - -#include "StarEngine/Renderer/Renderer.h" - -#include "Platform/OpenGL/OpenGLContext.h" - -namespace StarEngine { - - float Window::s_HighDPIScaleFactor = 1.0f; - - static uint8_t s_GLFWWindowCount = 0; - - static void GLFWErrorCallback(int error, const char* description) - { - SE_CORE_ERROR("GLFW Error ({0}): {1}", error, description); - } - - WindowsWindow::WindowsWindow(const WindowProps& props) - { - SE_PROFILE_FUNCTION(); - - Init(props); - } - - WindowsWindow::~WindowsWindow() - { - SE_PROFILE_FUNCTION(); - - Shutdown(); - } - - void WindowsWindow::Init(const WindowProps& props) - { - SE_PROFILE_FUNCTION(); - - m_Data.Title = props.Title; - m_Data.Width = props.Width; - m_Data.Height = props.Height; - - SE_CORE_INFO("Creating window {0} ({1}, {2})", props.Title, props.Width, props.Height); - - if (s_GLFWWindowCount == 0) - { - SE_PROFILE_SCOPE("glfwInit"); - int success = glfwInit(); - SE_CORE_ASSERT(success, "Could not initialize GLFW!"); - glfwSetErrorCallback(GLFWErrorCallback); - } - - { - SE_PROFILE_SCOPE("glfwCreateWindow"); - - GLFWmonitor* monitor = glfwGetPrimaryMonitor(); - float xscale, yscale; - glfwGetMonitorContentScale(monitor, &xscale, &yscale); - - if (xscale > 1.0f || yscale > 1.0f) - { - s_HighDPIScaleFactor = yscale; - glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); - } - - #if defined(SE_DEBUG) - if (Renderer::GetAPI() == RendererAPI::API::OpenGL) - glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); - #endif - - m_Window = glfwCreateWindow((int)props.Width, (int)props.Height, m_Data.Title.c_str(), nullptr, nullptr); - ++s_GLFWWindowCount; - } - - m_Context = GraphicsContext::Create(m_Window); - m_Context->Init(); - - glfwSetWindowUserPointer(m_Window, &m_Data); - SetVSync(true); - - // Set GLFW callbacks - glfwSetWindowSizeCallback(m_Window, [](GLFWwindow* window, int width, int height) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - data.Width = width; - data.Height = height; - - WindowResizeEvent event(width, height); - data.EventCallback(event); - }); - - glfwSetWindowCloseCallback(m_Window, [](GLFWwindow* window) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - WindowCloseEvent event; - data.EventCallback(event); - }); - - glfwSetKeyCallback(m_Window, [](GLFWwindow* window, int key, int scancode, int action, int mods) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - - switch (action) - { - case GLFW_PRESS: - { - KeyPressedEvent event(key, true); - data.EventCallback(event); - break; - } - case GLFW_RELEASE: - { - KeyReleasedEvent event(key); - data.EventCallback(event); - break; - } - case GLFW_REPEAT: - { - KeyPressedEvent event(key, 1); - data.EventCallback(event); - break; - } - } - }); - - glfwSetCharCallback(m_Window, [](GLFWwindow* window, unsigned int keycode) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - - KeyTypedEvent event(keycode); - data.EventCallback(event); - }); - - glfwSetMouseButtonCallback(m_Window, [](GLFWwindow* window, int button, int action, int mods) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - - switch (action) - { - case GLFW_PRESS: - { - MouseButtonPressedEvent event(button); - data.EventCallback(event); - break; - } - case GLFW_RELEASE: - { - MouseButtonReleasedEvent event(button); - data.EventCallback(event); - break; - } - } - }); - - glfwSetScrollCallback(m_Window, [](GLFWwindow* window, double xOffset, double yOffset) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - - MouseScrolledEvent event((float)xOffset, (float)yOffset); - data.EventCallback(event); - }); - - glfwSetCursorPosCallback(m_Window, [](GLFWwindow* window, double xPos, double yPos) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - - MouseMovedEvent event((float)xPos, (float)yPos); - data.EventCallback(event); - }); - - glfwSetDropCallback(m_Window, [](GLFWwindow* window, int pathCount, const char* paths[]) - { - WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); - - std::vector filepaths(pathCount); - for (int i = 0; i < pathCount; i++) - filepaths[i] = paths[i]; - - WindowDropEvent event(std::move(filepaths)); - data.EventCallback(event); - }); - } - - void WindowsWindow::Shutdown() - { - SE_PROFILE_FUNCTION(); - - glfwDestroyWindow(m_Window); - --s_GLFWWindowCount; - - if (s_GLFWWindowCount == 0) - { - glfwTerminate(); - } - } - - void WindowsWindow::OnUpdate() - { - SE_PROFILE_FUNCTION(); - - glfwPollEvents(); - m_Context->SwapBuffers(); - } - - void WindowsWindow::SetVSync(bool enabled) - { - SE_PROFILE_FUNCTION(); - - if (enabled) - glfwSwapInterval(1); - else - glfwSwapInterval(0); - - m_Data.VSync = enabled; - } - - bool WindowsWindow::IsVSync() const - { - return m_Data.VSync; - } - -} diff --git a/StarEngine/src/Platform/Windows/WindowsWindow.h b/StarEngine/src/Platform/Windows/WindowsWindow.h deleted file mode 100644 index 455677f5..00000000 --- a/StarEngine/src/Platform/Windows/WindowsWindow.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include "StarEngine/Core/Window.h" -#include "StarEngine/Renderer/GraphicsContext.h" - -#include - -namespace StarEngine { - - class WindowsWindow : public Window - { - public: - WindowsWindow(const WindowProps& props); - virtual ~WindowsWindow(); - - void OnUpdate() override; - - unsigned int GetWidth() const override { return m_Data.Width; } - unsigned int GetHeight() const override { return m_Data.Height; } - - // Window attributes - void SetEventCallback(const EventCallbackFn& callback) override { m_Data.EventCallback = callback; } - void SetVSync(bool enabled) override; - bool IsVSync() const override; - - virtual void* GetNativeWindow() const { return m_Window; } - private: - virtual void Init(const WindowProps& props); - virtual void Shutdown(); - private: - GLFWwindow* m_Window; - Scope m_Context; - - struct WindowData - { - std::string Title; - unsigned int Width, Height; - bool VSync; - - EventCallbackFn EventCallback; - }; - - WindowData m_Data; - }; - -} \ No newline at end of file diff --git a/StarEngine/src/StarEngine.h b/StarEngine/src/StarEngine.h index 6456c547..3de60a89 100644 --- a/StarEngine/src/StarEngine.h +++ b/StarEngine/src/StarEngine.h @@ -1,40 +1,44 @@ +// +// Note: this file is to be included in client applications ONLY +// NEVER include this file anywhere in the engine codebase +// #pragma once -// for use by StarEngine applications #include "StarEngine/Core/Application.h" -#include "StarEngine/Core/Layer.h" #include "StarEngine/Core/Log.h" -#include "StarEngine/Core/Assert.h" +#include "StarEngine/Core/Input.h" +#include "StarEngine/Core/TimeStep.h" +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Core/Platform.h" +#include "StarEngine/Core/Version.h" -#include "StarEngine/Core/Timestep.h" +#include "StarEngine/Core/Events/Event.h" +#include "StarEngine/Core/Events/ApplicationEvent.h" +#include "StarEngine/Core/Events/KeyEvent.h" +#include "StarEngine/Core/Events/MouseEvent.h" +#include "StarEngine/Core/Events/SceneEvents.h" -#include "StarEngine/ImGui/ImGuiLayer.h" +#include "StarEngine/Core/Math/AABB.h" +#include "StarEngine/Core/Math/Ray.h" -// ---Input------------------------- -#include "StarEngine/Core/Input.h" -#include "StarEngine/Core/KeyCodes.h" -#include "StarEngine/Core/MouseCodes.h" -// --------------------------------- +#include "imgui/imgui.h" -// ---Renderer------------------------ +// --- StarEngine Render API ------------------------------ #include "StarEngine/Renderer/Renderer.h" -#include "StarEngine/Renderer/Renderer2D.h" -#include "StarEngine/Renderer/RenderCommand.h" +#include "StarEngine/Renderer/SceneRenderer.h" +#include "StarEngine/Renderer/RenderPass.h" +#include "StarEngine/Renderer/Framebuffer.h" +#include "StarEngine/Renderer/VertexBuffer.h" +#include "StarEngine/Renderer/IndexBuffer.h" +#include "StarEngine/Renderer/Pipeline.h" +#include "StarEngine/Renderer/Texture.h" +#include "StarEngine/Renderer/Shader.h" +#include "StarEngine/Renderer/Camera.h" +#include "StarEngine/Renderer/Material.h" +// --------------------------------------------------- +// Scenes #include "StarEngine/Scene/Scene.h" -#include "StarEngine/Scene/Entity.h" +#include "StarEngine/Scene/SceneCamera.h" +#include "StarEngine/Scene/SceneSerializer.h" #include "StarEngine/Scene/Components.h" -#include "StarEngine/Scene/ScriptableEntity.h" - -#include "StarEngine/Project/Project.h" - -#include "StarEngine/Renderer/Buffer.h" -#include "StarEngine/Renderer/Shader.h" -#include "StarEngine/Renderer/Texture.h" -#include "StarEngine/Renderer/Framebuffer.h" -#include "StarEngine/Renderer/SubTexture2D.h" -#include "StarEngine/Renderer/VertexArray.h" - -#include "StarEngine/Renderer/OrthographicCamera.h" -#include "StarEngine/Renderer/OrthographicCameraController.h" -// ----------------------------------- diff --git a/StarEngine/src/StarEngine/Asset/Asset.cpp b/StarEngine/src/StarEngine/Asset/Asset.cpp deleted file mode 100644 index bcd14ff9..00000000 --- a/StarEngine/src/StarEngine/Asset/Asset.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "sepch.h" -#include "Asset.h" - -namespace StarEngine { - - std::string_view AssetTypeToString(AssetType type) - { - switch (type) - { - case AssetType::None: return "None"; - case AssetType::Scene: return "Scene"; - case AssetType::Texture2D: return "Texture2D"; - case AssetType::Audio: return "Audio";/* - case AssetType::ObjModel: return "ObjModel"; - case AssetType::ScriptFile: return "ScriptFile";*/ - } - - return ""; - } - - AssetType AssetTypeFromString(std::string_view assetType) - { - if (assetType == "None") return AssetType::None; - if (assetType == "Scene") return AssetType::Scene; - if (assetType == "Texture2D") return AssetType::Texture2D; - if (assetType == "Audio") return AssetType::Audio;/* - if (assetType == "ObjModel") return AssetType::ObjModel; - if (assetType == "ScriptFile") return AssetType::ScriptFile;*/ - - return AssetType::None; - } - -} diff --git a/StarEngine/src/StarEngine/Asset/Asset.h b/StarEngine/src/StarEngine/Asset/Asset.h index e5d31692..1b27db85 100644 --- a/StarEngine/src/StarEngine/Asset/Asset.h +++ b/StarEngine/src/StarEngine/Asset/Asset.h @@ -1,32 +1,96 @@ #pragma once #include "StarEngine/Core/UUID.h" - -#include +#include "StarEngine/Asset/AssetTypes.h" namespace StarEngine { using AssetHandle = UUID; - enum class AssetType : uint16_t + class Asset : public RefCounted { - None = 0, - Scene, - Texture2D, - Audio,/* - ObjModel, - ScriptFile,*/ + public: + AssetHandle Handle = 0; + uint16_t Flags = (uint16_t)AssetFlag::None; + + virtual ~Asset() {} + + static AssetType GetStaticType() { return AssetType::None; } + virtual AssetType GetAssetType() const { return AssetType::None; } + + virtual void OnDependencyUpdated(AssetHandle handle) {} + + virtual bool operator==(const Asset& other) const + { + return Handle == other.Handle; + } + + virtual bool operator!=(const Asset& other) const + { + return !(*this == other); + } + + private: + // If you want to find out whether assets are valid or missing, use AssetManager::IsAssetValid(handle), IsAssetMissing(handle) + // This cleans up and removes inconsistencies from rest of the code. + // You simply go AssetManager::GetAsset(handle), and so long as you get a non-null pointer back, you're good to go. + // No IsValid(), IsFlagSet(AssetFlag::Missing) etc. etc. all throughout the code. + friend class EditorAssetManager; + friend class RuntimeAssetManager; + friend class AssimpMeshImporter; + friend class TextureSerializer; + + bool IsValid() const { return ((Flags & (uint16_t)AssetFlag::Missing) | (Flags & (uint16_t)AssetFlag::Invalid)) == 0; } + + bool IsFlagSet(AssetFlag flag) const { return (uint16_t)flag & Flags; } + void SetFlag(AssetFlag flag, bool value = true) + { + if (value) + Flags |= (uint16_t)flag; + else + Flags &= ~(uint16_t)flag; + } }; - std::string_view AssetTypeToString(AssetType type); - AssetType AssetTypeFromString(std::string_view assetType); - class Asset + // Note (0x): this does not belong here. + class AudioFile : public Asset { public: - AssetHandle Handle; // Generate handle + double Duration; + uint32_t SamplingRate; + uint16_t BitDepth; + uint16_t NumChannels; + uint64_t FileSize; + + AudioFile() = default; + AudioFile(double duration, uint32_t samplingRate, uint16_t bitDepth, uint16_t numChannels, uint64_t fileSize) + : Duration(duration), SamplingRate(samplingRate), BitDepth(bitDepth), NumChannels(numChannels), FileSize(fileSize) + { + } + + static AssetType GetStaticType() { return AssetType::Audio; } + virtual AssetType GetAssetType() const override { return GetStaticType(); } + }; + + template + struct AsyncAssetResult + { + Ref Asset; + bool IsReady = false; + + AsyncAssetResult() = default; + AsyncAssetResult(const AsyncAssetResult& other) = default; + + AsyncAssetResult(Ref asset, bool isReady = false) + : Asset(asset), IsReady(isReady) {} + + template + AsyncAssetResult(const AsyncAssetResult& other) + : Asset(other.Asset.template As()), IsReady(other.IsReady) {} - virtual AssetType GetType() const = 0; + operator Ref() const { return Asset; } + operator bool() const { return IsReady; } }; } diff --git a/StarEngine/src/StarEngine/Asset/AssetExtensions.h b/StarEngine/src/StarEngine/Asset/AssetExtensions.h new file mode 100644 index 00000000..dda0ec01 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetExtensions.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "AssetTypes.h" + +namespace StarEngine { + + inline static std::unordered_map s_AssetExtensionMap = + { + // StarEngine types + { ".sscene", AssetType::Scene }, + { ".smesh", AssetType::Mesh }, + { ".ssmesh", AssetType::StaticMesh }, + { ".smaterial", AssetType::Material }, + { ".sskel", AssetType::Skeleton }, + { ".sanim", AssetType::Animation }, + { ".sanimgraph", AssetType::AnimationGraph }, + { ".sprefab", AssetType::Prefab }, + { ".ssoundc", AssetType::SoundConfig }, + + { ".cs", AssetType::ScriptFile }, + + // mesh/animation source + { ".fbx", AssetType::MeshSource }, + { ".gltf", AssetType::MeshSource }, + { ".glb", AssetType::MeshSource }, + { ".obj", AssetType::MeshSource }, + { ".dae", AssetType::MeshSource }, + + // Textures + { ".png", AssetType::Texture }, + { ".jpg", AssetType::Texture }, + { ".jpeg", AssetType::Texture }, + { ".hdr", AssetType::EnvMap }, + + // Audio + { ".wav", AssetType::Audio }, + { ".ogg", AssetType::Audio }, + + // Fonts + { ".ttf", AssetType::Font }, + { ".ttc", AssetType::Font }, + { ".otf", AssetType::Font }, + + // Mesh Collider + { ".smc", AssetType::MeshCollider }, + + // Graphs + { ".sound_graph", AssetType::SoundGraphSound } + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetImporter.cpp b/StarEngine/src/StarEngine/Asset/AssetImporter.cpp index 38679a16..c3cd3966 100644 --- a/StarEngine/src/StarEngine/Asset/AssetImporter.cpp +++ b/StarEngine/src/StarEngine/Asset/AssetImporter.cpp @@ -1,49 +1,109 @@ #include "sepch.h" #include "AssetImporter.h" +#include "AssetManager.h" +#include "MeshSerializer.h" -#include "TextureImporter.h" -#include "SceneImporter.h" -#include "AudioImporter.h" +#include "StarEngine/Debug/Profiler.h" -/* -#include "FontImporter.h" -#include "ObjModelImporter.h"*/ +namespace StarEngine { -#include + void AssetImporter::Init() + { + s_Serializers.clear(); + s_Serializers[AssetType::Prefab] = CreateScope(); + s_Serializers[AssetType::Texture] = CreateScope(); + s_Serializers[AssetType::Mesh] = CreateScope(); + s_Serializers[AssetType::StaticMesh] = CreateScope(); + s_Serializers[AssetType::MeshSource] = CreateScope(); + s_Serializers[AssetType::Material] = CreateScope(); + s_Serializers[AssetType::EnvMap] = CreateScope(); + s_Serializers[AssetType::Audio] = CreateScope(); + s_Serializers[AssetType::SoundConfig] = CreateScope(); + s_Serializers[AssetType::Scene] = CreateScope(); + s_Serializers[AssetType::Font] = CreateScope(); + s_Serializers[AssetType::MeshCollider] = CreateScope(); + s_Serializers[AssetType::ScriptFile] = CreateScope(); + } -namespace StarEngine { + void AssetImporter::Serialize(const AssetMetadata& metadata, const Ref& asset) + { + if (s_Serializers.find(metadata.Type) == s_Serializers.end()) + { + SE_CORE_WARN("There's currently no importer for assets of type {0}", metadata.FilePath.stem().string()); + return; + } - using AssetImportFunction = std::function(AssetHandle, const AssetMetadata&)>; - static std::map s_AssetImportFunctions = { - { AssetType::Scene, SceneImporter::ImportScene }, - { AssetType::Texture2D, TextureImporter::ImportTexture2D }, - { AssetType::Audio, AudioImporter::ImportAudio },/* - { AssetType::ObjModel, ObjModelImporter::ImportObjModel }, - { AssetType::ScriptFile, SceneImporter::ImportScript }*/ - }; + s_Serializers[asset->GetAssetType()]->Serialize(metadata, asset); + } - Ref AssetImporter::ImportAsset(AssetHandle handle, const AssetMetadata& metadata) + void AssetImporter::Serialize(const Ref& asset) { - SE_PROFILE_FUNCTION_COLOR("AssetImporter::ImportAsset", 0xF2FA8A); + const AssetMetadata& metadata = Project::GetEditorAssetManager()->GetMetadata(asset->Handle); + Serialize(metadata, asset); + } - { - SE_PROFILE_SCOPE_COLOR("AssetImporter::ImportAsset Scope", 0x27628A); + bool AssetImporter::TryLoadData(const AssetMetadata& metadata, Ref& asset) + { + SE_PROFILE_FUNCTION("AssetImporter::TryLoadData"); - if (s_AssetImportFunctions.find(metadata.Type) == s_AssetImportFunctions.end()) - { - SE_CORE_ERROR("No importer available for asset type: {}", (uint16_t)metadata.Type); - return nullptr; - } + if (s_Serializers.find(metadata.Type) == s_Serializers.end()) + { + SE_CORE_WARN("There's currently no importer for assets of type {0}", metadata.FilePath.stem().string()); + return false; } - auto& result = s_AssetImportFunctions.at(metadata.Type);//(metadata.Type)(handle, metadata); + // HZ_CORE_TRACE("AssetImporter::TryLoadData - {}", metadata.FilePath); + return s_Serializers[metadata.Type]->TryLoadData(metadata, asset); + } + void AssetImporter::RegisterDependencies(const AssetMetadata& metadata) + { + if (s_Serializers.find(metadata.Type) == s_Serializers.end()) { - SE_PROFILE_SCOPE_COLOR("AssetImporter::ImportAsset 2 Scope", 0xD1C48A); + SE_CORE_WARN("There's currently no importer for assets of type {0}", metadata.FilePath.stem().string()); + return; + } - return result(handle, metadata); + s_Serializers[metadata.Type]->RegisterDependencies(metadata); + } + + bool AssetImporter::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) + { + outInfo.Size = 0; + + if (!AssetManager::IsAssetHandleValid(handle)) + return false; + + AssetType type = AssetManager::GetAssetType(handle); + if (s_Serializers.find(type) == s_Serializers.end()) + { + const auto& metadata = Project::GetEditorAssetManager()->GetMetadata(handle); + SE_CORE_WARN("There's currently no serializer for assets of type {0}", metadata.FilePath.stem().string()); + return false; } + return s_Serializers[type]->SerializeToAssetPack(handle, stream, outInfo); } + Ref AssetImporter::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) + { + AssetType assetType = (AssetType)assetInfo.Type; + if (s_Serializers.find(assetType) == s_Serializers.end()) + return nullptr; + + return s_Serializers[assetType]->DeserializeFromAssetPack(stream, assetInfo); + } + + Ref AssetImporter::DeserializeSceneFromAssetPack(FileStreamReader& stream, const AssetPackFile::SceneInfo& sceneInfo) + { + AssetType assetType = AssetType::Scene; + if (s_Serializers.find(assetType) == s_Serializers.end()) + return nullptr; + + SceneAssetSerializer* sceneAssetSerializer = (SceneAssetSerializer*)s_Serializers[assetType].get(); + return sceneAssetSerializer->DeserializeSceneFromAssetPack(stream, sceneInfo); + } + + std::unordered_map> AssetImporter::s_Serializers; + } diff --git a/StarEngine/src/StarEngine/Asset/AssetImporter.h b/StarEngine/src/StarEngine/Asset/AssetImporter.h index da037819..4a2fcd4f 100644 --- a/StarEngine/src/StarEngine/Asset/AssetImporter.h +++ b/StarEngine/src/StarEngine/Asset/AssetImporter.h @@ -1,14 +1,26 @@ #pragma once -#include "AssetMetadata.h" +#include "AssetSerializer.h" + +#include "StarEngine/Serialization/FileStream.h" +#include "StarEngine/Scene/Scene.h" + +namespace StarEngine { -namespace StarEngine -{ class AssetImporter { public: - static Ref ImportAsset(AssetHandle handle, const AssetMetadata& metadata); + static void Init(); + static void Serialize(const AssetMetadata& metadata, const Ref& asset); + static void Serialize(const Ref& asset); + static bool TryLoadData(const AssetMetadata& metadata, Ref& asset); + static void RegisterDependencies(const AssetMetadata& metadata); + + static bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo); + static Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo); + static Ref DeserializeSceneFromAssetPack(FileStreamReader& stream, const AssetPackFile::SceneInfo& assetInfo); + private: + static std::unordered_map> s_Serializers; }; - } diff --git a/StarEngine/src/StarEngine/Asset/AssetManager.cpp b/StarEngine/src/StarEngine/Asset/AssetManager.cpp index 15c19c38..4d160060 100644 --- a/StarEngine/src/StarEngine/Asset/AssetManager.cpp +++ b/StarEngine/src/StarEngine/Asset/AssetManager.cpp @@ -1,7 +1,23 @@ #include "sepch.h" #include "AssetManager.h" -namespace StarEngine -{ - +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Renderer/UI/Font.h" + +namespace StarEngine { + + static std::unordered_map()>> s_AssetPlaceholderTable = + { + { AssetType::Texture, []() { return Renderer::GetWhiteTexture(); }}, + { AssetType::EnvMap, []() { return Renderer::GetEmptyEnvironment(); }}, + { AssetType::Font, []() { return Font::GetDefaultFont(); }}, + }; + + Ref AssetManager::GetPlaceholderAsset(AssetType type) + { + if (s_AssetPlaceholderTable.contains(type)) + return s_AssetPlaceholderTable.at(type)(); + + return nullptr; + } } diff --git a/StarEngine/src/StarEngine/Asset/AssetManager.h b/StarEngine/src/StarEngine/Asset/AssetManager.h index 9c8b24fa..b0fe488d 100644 --- a/StarEngine/src/StarEngine/Asset/AssetManager.h +++ b/StarEngine/src/StarEngine/Asset/AssetManager.h @@ -1,36 +1,115 @@ #pragma once -#include "AssetManagerBase.h" - +#include "StarEngine/Asset/Asset.h" +#include "StarEngine/Asset/AssetTypes.h" +#include "StarEngine/Core/Application.h" #include "StarEngine/Project/Project.h" +#include "StarEngine/Utilities/FileSystem.h" + +#include +#include +#include + +// Asynchronous asset loading can be disabled by setting this to 0 +// If you do this, then assets will not be automatically reloaded if/when they are changed by some external tool, +// and you will have to manually reload them via content browser panel. +#define ASYNC_ASSETS 1 + +namespace StarEngine { -namespace StarEngine -{ class AssetManager { public: + // Returns true if assetHandle could potentially be valid. + static bool IsAssetHandleValid(AssetHandle assetHandle) { return Project::GetAssetManager()->IsAssetHandleValid(assetHandle); } + + // Returns true if the asset referred to by assetHandle is valid. + // Note that this will attempt to load the asset if it is not already loaded. + // An asset is invalid if any of the following are true: + // - The asset handle is invalid + // - The file referred to by asset meta data is missing + // - The asset could not be loaded from file + static bool IsAssetValid(AssetHandle assetHandle) { return Project::GetAssetManager()->IsAssetValid(assetHandle); } + + // Returns true if the asset referred to by assetHandle is missing. + // Note that this checks for existence of file, but makes no attempt to load the asset from file + // A memory-only asset cannot be missing. + static bool IsAssetMissing(AssetHandle assetHandle) { return Project::GetAssetManager()->IsAssetMissing(assetHandle); } + + static bool IsMemoryAsset(AssetHandle handle) { return Project::GetAssetManager()->IsMemoryAsset(handle); } + static bool IsPhysicalAsset(AssetHandle handle) { return Project::GetAssetManager()->IsPhysicalAsset(handle); } + + static bool ReloadData(AssetHandle assetHandle) { return Project::GetAssetManager()->ReloadData(assetHandle); } + static bool EnsureCurrent(AssetHandle assetHandle) { return Project::GetAssetManager()->EnsureCurrent(assetHandle); } + static bool EnsureAllLoadedCurrent() { return Project::GetAssetManager()->EnsureAllLoadedCurrent(); } + + static AssetType GetAssetType(AssetHandle assetHandle) { return Project::GetAssetManager()->GetAssetType(assetHandle); } + + static void SyncWithAssetThread() { return Project::GetAssetManager()->SyncWithAssetThread(); } + + static Ref GetPlaceholderAsset(AssetType type); + template - static Ref GetAsset(AssetHandle handle) + static Ref GetAsset(AssetHandle assetHandle) { - //SE_PROFILE_FUNCTION_COLOR("AssetManager::GetAsset", 0x8CCBFF); + //static std::mutex mutex; + //std::scoped_lock lock(mutex); - Ref asset = Project::GetActive()->GetAssetManager()->GetAsset(handle); - return std::static_pointer_cast(asset); + Ref asset = Project::GetAssetManager()->GetAsset(assetHandle); + return asset.As(); } - static bool IsAssetHandleValid(AssetHandle handle) + template + static AsyncAssetResult GetAssetAsync(AssetHandle assetHandle) { - return Project::GetActive()->GetAssetManager()->IsAssetHandleValid(handle); +#if ASYNC_ASSETS + AsyncAssetResult result = Project::GetAssetManager()->GetAssetAsync(assetHandle); + return AsyncAssetResult(result); +#else + return { GetAsset(assetHandle), true }; +#endif } - static bool IsAssetLoaded(AssetHandle handle) + template + static std::unordered_set GetAllAssetsWithType() { - return Project::GetActive()->GetAssetManager()->IsAssetLoaded(handle); + return Project::GetAssetManager()->GetAllAssetsWithType(T::GetStaticType()); } - static AssetType GetAssetType(AssetHandle handle) + static const std::unordered_map>& GetLoadedAssets() { return Project::GetAssetManager()->GetLoadedAssets(); } + + // Note (0x): The memory-only asset must be fully initialised before you AddMemoryOnlyAsset() + // Assets are not themselves thread-safe, but can potentially be accessed from multiple + // threads. Thread safety therefore depends on the assets being immutable once they've been + // added to the asset manager. + template + static AssetHandle AddMemoryOnlyAsset(Ref asset) { - return Project::GetActive()->GetAssetManager()->GetAssetType(handle); + static_assert(std::is_base_of::value, "AddMemoryOnlyAsset only works for types derived from Asset"); + if (!asset->Handle) + { + asset->Handle = AssetHandle(); // NOTE(Yan): should handle generation happen here? + } + Project::GetAssetManager()->AddMemoryOnlyAsset(asset); + return asset->Handle; } + + static Ref GetMemoryAsset(AssetHandle handle) { return Project::GetAssetManager()->GetMemoryAsset(handle); } + + // handle is dependent on dependency. e.g. handle could be a material, and dependency could be a texture that the material uses. + static void RegisterDependency(AssetHandle dependency, AssetHandle handle) { return Project::GetAssetManager()->RegisterDependency(dependency, handle); } + + // remove dependency of handle on dependency + static void DeregisterDependency(AssetHandle dependency, AssetHandle handle) { return Project::GetAssetManager()->DeregisterDependency(dependency, handle); } + + // remove all dependencies of handle + static void DeregisterDependencies(AssetHandle handle) { return Project::GetAssetManager()->DeregisterDependencies(handle); } + + static void RemoveAsset(AssetHandle handle) + { + Project::GetAssetManager()->RemoveAsset(handle); + } + }; + } diff --git a/StarEngine/src/StarEngine/Asset/AssetManager/AssetManagerBase.h b/StarEngine/src/StarEngine/Asset/AssetManager/AssetManagerBase.h new file mode 100644 index 00000000..9b59eace --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetManager/AssetManagerBase.h @@ -0,0 +1,64 @@ +#pragma once + +#include "StarEngine/Asset/Asset.h" +#include "StarEngine/Asset/AssetTypes.h" + +#include +#include + +namespace StarEngine { + + + + ////////////////////////////////////////////////////////////////// + // AssetManagerBase ////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + // Implementation in RuntimeAssetManager and EditorAssetManager // + // Static wrapper in AssetManager //////////////////////////////// + ////////////////////////////////////////////////////////////////// + class AssetManagerBase : public RefCounted + { + public: + AssetManagerBase() = default; + virtual ~AssetManagerBase() = default; + + virtual void Shutdown() = 0; + + virtual AssetType GetAssetType(AssetHandle assetHandle) = 0; + virtual Ref GetAsset(AssetHandle assetHandle) = 0; + virtual AsyncAssetResult GetAssetAsync(AssetHandle assetHandle) = 0; + + virtual void AddMemoryOnlyAsset(Ref asset) = 0; + virtual bool ReloadData(AssetHandle assetHandle) = 0; + virtual void ReloadDataAsync(AssetHandle assetHandle) = 0; + virtual bool EnsureCurrent(AssetHandle assetHandle) = 0; + virtual bool EnsureAllLoadedCurrent() = 0; + virtual bool IsAssetHandleValid(AssetHandle assetHandle) = 0; // the asset handle is valid (this says nothing about the asset itself) + virtual Ref GetMemoryAsset(AssetHandle handle) = 0; // if exists in memory only (i.e. there is no backing file) return it otherwise return nullptr (this is more efficient than IsMemoryAsset() followed by GetAsset()) + virtual bool IsAssetLoaded(AssetHandle handle) = 0; // asset has been loaded from file (it could still be invalid) + virtual bool IsAssetValid(AssetHandle handle) = 0; // asset file was loaded, but is invalid for some reason (e.g. corrupt file) + virtual bool IsAssetMissing(AssetHandle handle) = 0; // asset file is missing + virtual bool IsMemoryAsset(AssetHandle handle) = 0; + virtual bool IsPhysicalAsset(AssetHandle handle) = 0; + virtual void RemoveAsset(AssetHandle handle) = 0; + + // handle is dependent on dependency. e.g. handle could be a material, and dependency could be a texture that the material uses. + virtual void RegisterDependency(AssetHandle dependency, AssetHandle handle) = 0; + + // remove dependency of handle on dependency + virtual void DeregisterDependency(AssetHandle dependency, AssetHandle handle) = 0; + + // remove all dependencies of handle + virtual void DeregisterDependencies(AssetHandle handle) = 0; + + + // get the dependencies of handle. e.g. handle could be a material, GetDependencies(handle) returns all the textures that the material uses. + virtual std::unordered_set GetDependencies(AssetHandle handle) = 0; + + virtual void SyncWithAssetThread() = 0; + + virtual std::unordered_set GetAllAssetsWithType(AssetType type) = 0; + virtual const std::unordered_map>& GetLoadedAssets() = 0; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetManager/EditorAssetManager.cpp b/StarEngine/src/StarEngine/Asset/AssetManager/EditorAssetManager.cpp new file mode 100644 index 00000000..ce9f9040 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetManager/EditorAssetManager.cpp @@ -0,0 +1,789 @@ +#include "sepch.h" +#include "EditorAssetManager.h" + +#include "StarEngine/Asset/AssetExtensions.h" +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Core/Application.h" +#include "StarEngine/Core/Events/EditorEvents.h" +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Debug/Profiler.h" +#include "StarEngine/Project/Project.h" +#include "StarEngine/Utilities/StringUtils.h" + +#include + +namespace StarEngine { + + static AssetMetadata s_NullMetadata; + + EditorAssetManager::EditorAssetManager() + { +#if ASYNC_ASSETS + m_AssetThread = Ref::Create(); +#endif + + AssetImporter::Init(); + + LoadAssetRegistry(); + ReloadAssets(); + } + + EditorAssetManager::~EditorAssetManager() + { + // TODO(Yan): shutdown explicitly? + Shutdown(); + } + + void EditorAssetManager::Shutdown() + { +#if ASYNC_ASSETS + m_AssetThread->StopAndWait(); +#endif + WriteRegistryToFile(); + } + + AssetType EditorAssetManager::GetAssetType(AssetHandle assetHandle) + { + if (!IsAssetHandleValid(assetHandle)) + return AssetType::None; + + if (IsMemoryAsset(assetHandle)) + return GetAsset(assetHandle)->GetAssetType(); + + const auto& metadata = GetMetadata(assetHandle); + return metadata.Type; + } + + Ref EditorAssetManager::GetAsset(AssetHandle assetHandle) + { + SE_PROFILE_FUNCTION("EditorAssetManager::GetAsset"); + SE_SCOPE_PERF("AssetManager::GetAsset"); + + Ref asset = GetAssetIncludingInvalid(assetHandle); + return asset && asset->IsValid() ? asset : nullptr; + } + + AsyncAssetResult EditorAssetManager::GetAssetAsync(AssetHandle assetHandle) + { +#if ASYNC_ASSETS + SE_PROFILE_FUNCTION("EditorAssetManager::GetAssetAsync"); + SE_SCOPE_PERF("AssetManager::GetAssetAsync"); + + if (auto asset = GetMemoryAsset(assetHandle); asset) + return { asset, true }; + + auto metadata = GetMetadata(assetHandle); + if (!metadata.IsValid()) + return { nullptr }; // TODO(Yan): return special error asset + + Ref asset = nullptr; + if (metadata.IsDataLoaded) + { + SE_CORE_VERIFY(m_LoadedAssets.contains(assetHandle)); + return { m_LoadedAssets.at(assetHandle), true }; + } + + // Queue load (if not already) and return placeholder + if (metadata.Status != AssetStatus::Loading) + { + auto metadataLoad = metadata; + metadataLoad.Status = AssetStatus::Loading; + SetMetadata(assetHandle, metadataLoad); + m_AssetThread->QueueAssetLoad(metadata); + } + + return AssetManager::GetPlaceholderAsset(metadata.Type); +#else + return { GetAsset(assetHandle), true }; +#endif + } + + void EditorAssetManager::AddMemoryOnlyAsset(Ref asset) + { + // Memory-only assets are not added to m_AssetRegistry (because that would require full thread synchronization for access to registry, we would like to avoid that) + std::scoped_lock lock(m_MemoryAssetsMutex); + m_MemoryAssets[asset->Handle] = asset; + } + + std::unordered_set EditorAssetManager::GetAllAssetsWithType(AssetType type) + { + std::unordered_set result; + + // loop over memory only assets + // This needs a lock because asset thread can create memory only assets + { + std::shared_lock lock(m_MemoryAssetsMutex); + for (const auto& [handle, asset] : m_MemoryAssets) + { + if (asset->GetAssetType() == type) + result.insert(handle); + } + } + + { + std::shared_lock lock(m_AssetRegistryMutex); + for (const auto& [handle, metadata] : m_AssetRegistry) + { + if (metadata.Type == type) + result.insert(handle); + } + } + return result; + } + + std::unordered_map> EditorAssetManager::GetMemoryAssets() + { + std::shared_lock lock(m_MemoryAssetsMutex); + return m_MemoryAssets; + } + + AssetMetadata EditorAssetManager::GetMetadata(AssetHandle handle) + { + std::shared_lock lock(m_AssetRegistryMutex); + + if (m_AssetRegistry.Contains(handle)) + return m_AssetRegistry.Get(handle); + + return s_NullMetadata; + } + + void EditorAssetManager::SetMetadata(AssetHandle handle, const AssetMetadata& metadata) + { + std::unique_lock lock(m_AssetRegistryMutex); + m_AssetRegistry.Set(handle, metadata); + } + + + AssetHandle EditorAssetManager::GetAssetHandleFromFilePath(const std::filesystem::path& filepath) + { + const auto relativePath = GetRelativePath(filepath); + std::shared_lock lock(m_AssetRegistryMutex); + for (auto& [handle, metadata] : m_AssetRegistry) + { + if (metadata.FilePath == relativePath) + { + return metadata.Handle; + } + } + return 0; + } + + + AssetType EditorAssetManager::GetAssetTypeFromExtension(const std::string& extension) + { + std::string ext = Utils::String::ToLowerCopy(extension); + if (s_AssetExtensionMap.find(ext) == s_AssetExtensionMap.end()) + return AssetType::None; + + return s_AssetExtensionMap.at(ext.c_str()); + } + + std::string EditorAssetManager::GetDefaultExtensionForAssetType(AssetType type) + { + for (const auto& [ext, assetType] : s_AssetExtensionMap) + { + if (assetType == type) + return ext; + } + return ""; + } + + AssetType EditorAssetManager::GetAssetTypeFromPath(const std::filesystem::path& path) + { + return GetAssetTypeFromExtension(path.extension().string()); + } + + std::filesystem::path EditorAssetManager::GetFileSystemPath(const AssetMetadata& metadata) + { + return Project::GetActiveAssetDirectory() / metadata.FilePath; + } + + std::filesystem::path EditorAssetManager::GetFileSystemPath(AssetHandle handle) + { + return GetFileSystemPathString(GetMetadata(handle)); + } + + std::string EditorAssetManager::GetFileSystemPathString(const AssetMetadata& metadata) + { + return GetFileSystemPath(metadata).string(); + } + + std::filesystem::path EditorAssetManager::GetRelativePath(const std::filesystem::path& filepath) + { + std::filesystem::path relativePath = filepath.lexically_normal(); + std::string temp = filepath.string(); + if (temp.find(Project::GetActiveAssetDirectory().string()) != std::string::npos) + { + relativePath = std::filesystem::relative(filepath, Project::GetActiveAssetDirectory()); + if (relativePath.empty()) + { + relativePath = filepath.lexically_normal(); + } + } + return relativePath; + } + + bool EditorAssetManager::FileExists(AssetMetadata& metadata) const + { + return FileSystem::Exists(Project::GetActive()->GetAssetDirectory() / metadata.FilePath); + } + + bool EditorAssetManager::ReloadData(AssetHandle assetHandle) + { + auto metadata = GetMetadata(assetHandle); + if (!metadata.IsValid()) + { + SE_CORE_ERROR("Trying to reload invalid asset"); + return false; + } + + Ref asset = GetAsset(assetHandle); + + // If the asset is a Mesh, StaticMesh, Skeleton, or Animation, then instead of reloading the mesh we reload + // the underlying mesh source. + // (the assumption being that its the mesh source that's likely changed (e.g. via DCC authoring tool) and + // its that content that the user wishes to reload) + // The Mesh/StaticMesh/Skeleton/Animation ends up getting reloaded anyway due asset dependencies) + if (metadata.Type == AssetType::StaticMesh && asset) + { + auto mesh = asset.As(); + return ReloadData(mesh->GetMeshSource()); + } + if (metadata.Type == AssetType::Mesh && asset) + { + auto mesh = asset.As(); + return ReloadData(mesh->GetMeshSource()); + }/* + else if (metadata.Type == AssetType::Skeleton && asset) + { + auto skeleton = asset.As(); + return ReloadData(skeleton->GetMeshSource()); + } + else if (metadata.Type == AssetType::Animation && asset) + { + auto animation = asset.As(); + return ReloadData(animation->GetSkeletonSource()) && ((animation->GetAnimationSource() == animation->GetSkeletonSource()) || ReloadData(animation->GetAnimationSource())); + }*/ + else + { + SE_CORE_INFO_TAG("AssetManager", "RELOADING ASSET - {}", metadata.FilePath.string()); + metadata.IsDataLoaded = AssetImporter::TryLoadData(metadata, asset); + if (metadata.IsDataLoaded) + { + auto absolutePath = GetFileSystemPath(metadata); + metadata.FileLastWriteTime = FileSystem::GetLastWriteTime(absolutePath); + m_LoadedAssets[assetHandle] = asset; + SetMetadata(assetHandle, metadata); + SE_CORE_INFO_TAG("AssetManager", "Finished reloading asset {}", metadata.FilePath.string()); + UpdateDependents(assetHandle); + Application::Get().DispatchEvent(assetHandle); + } + else + { + SE_CORE_ERROR_TAG("AssetManager", "Failed to reload asset {}", metadata.FilePath.string()); + } + } + + return metadata.IsDataLoaded; + } + + void EditorAssetManager::ReloadDataAsync(AssetHandle assetHandle) + { +#if ASYNC_ASSETS + // Queue load (if not already) + auto metadata = GetMetadata(assetHandle); + if (!metadata.IsValid()) + { + SE_CORE_ERROR("Trying to reload invalid asset"); + return; + } + + if (metadata.Status != AssetStatus::Loading) + { + m_AssetThread->QueueAssetLoad(metadata); + metadata.Status = AssetStatus::Loading; + SetMetadata(assetHandle, metadata); + } +#else + ReloadData(assetHandle); +#endif + } + + // Returns true if asset was reloaded + bool EditorAssetManager::EnsureCurrent(AssetHandle assetHandle) + { + const auto& metadata = GetMetadata(assetHandle); + auto absolutePath = GetFileSystemPath(metadata); + + if (!FileSystem::Exists(absolutePath)) + return false; + + uint64_t actualLastWriteTime = FileSystem::GetLastWriteTime(absolutePath); + uint64_t recordedLastWriteTime = metadata.FileLastWriteTime; + + if (actualLastWriteTime == recordedLastWriteTime) + return false; + + return ReloadData(assetHandle); + } + + bool EditorAssetManager::EnsureAllLoadedCurrent() + { + SE_PROFILE_FUNCTION("EditorAssetManager::EnsureAllLoadedCurrent"); + + bool loaded = false; + for (const auto& [handle, asset] : m_LoadedAssets) + { + loaded |= EnsureCurrent(handle); + } + return loaded; + } + + Ref EditorAssetManager::GetMemoryAsset(AssetHandle handle) + { + std::shared_lock lock(m_MemoryAssetsMutex); + if (auto it = m_MemoryAssets.find(handle); it != m_MemoryAssets.end()) + return it->second; + + return nullptr; + } + + bool EditorAssetManager::IsAssetLoaded(AssetHandle handle) + { + return m_LoadedAssets.contains(handle); + } + + bool EditorAssetManager::IsAssetValid(AssetHandle handle) + { + SE_PROFILE_FUNCTION("EditorAssetManager::IsAssetValid"); + SE_SCOPE_PERF("AssetManager::IsAssetValid"); + + auto asset = GetAssetIncludingInvalid(handle); + return asset && asset->IsValid(); + } + + bool EditorAssetManager::IsAssetMissing(AssetHandle handle) + { + SE_PROFILE_FUNCTION("EditorAssetManager::IsAssetMissing"); + SE_SCOPE_PERF("AssetManager::IsAssetMissing"); + + if(GetMemoryAsset(handle)) + return false; + + auto metadata = GetMetadata(handle); + return !FileSystem::Exists(Project::GetActive()->GetAssetDirectory() / metadata.FilePath); + } + + bool EditorAssetManager::IsMemoryAsset(AssetHandle handle) + { + std::scoped_lock lock(m_MemoryAssetsMutex); + return m_MemoryAssets.contains(handle); + } + + bool EditorAssetManager::IsPhysicalAsset(AssetHandle handle) + { + return !IsMemoryAsset(handle); + } + + void EditorAssetManager::RemoveAsset(AssetHandle handle) + { + { + std::scoped_lock lock(m_MemoryAssetsMutex); + if (m_MemoryAssets.contains(handle)) + m_MemoryAssets.erase(handle); + } + + if (m_LoadedAssets.contains(handle)) + m_LoadedAssets.erase(handle); + + { + std::scoped_lock lock(m_AssetRegistryMutex); + if (m_AssetRegistry.Contains(handle)) + m_AssetRegistry.Remove(handle); + } + } + + // handle is dependent on dependency + void EditorAssetManager::RegisterDependency(AssetHandle dependency, AssetHandle handle) + { + std::scoped_lock lock(m_AssetDependenciesMutex); + + if (dependency != 0) + { + SE_CORE_ASSERT(handle != 0); + m_AssetDependents[dependency].insert(handle); + m_AssetDependencies[handle].insert(dependency); + return; + } + + // otherwise just make sure there is an entry in m_AssetDependencies for handle + if (m_AssetDependencies.find(handle) == m_AssetDependencies.end()) + { + m_AssetDependencies[handle] = {}; + } + } + + // handle is no longer dependent on dependency + void EditorAssetManager::DeregisterDependency(AssetHandle dependency, AssetHandle handle) + { + std::scoped_lock lock(m_AssetDependenciesMutex); + if (dependency != 0) + { + m_AssetDependents[dependency].erase(handle); + m_AssetDependencies[handle].erase(dependency); + } + } + + void EditorAssetManager::DeregisterDependencies(AssetHandle handle) + { + std::scoped_lock lock(m_AssetDependenciesMutex); + if (auto it = m_AssetDependencies.find(handle); it != m_AssetDependencies.end()) + { + for (AssetHandle dependency : it->second) + { + m_AssetDependents[dependency].erase(handle); + } + m_AssetDependencies.erase(it); + } + } + + std::unordered_set EditorAssetManager::GetDependencies(AssetHandle handle) + { + bool registered = false; + std::unordered_set result; + { + std::shared_lock lock(m_AssetDependenciesMutex); + if (auto it = m_AssetDependencies.find(handle); it != m_AssetDependencies.end()) + { + registered = true; + result = it->second; + } + } + + if (!registered) + { + if (auto metadata = GetMetadata(handle); metadata.IsValid()) + { + AssetImporter::RegisterDependencies(metadata); + { + std::shared_lock lock(m_AssetDependenciesMutex); + if (auto it = m_AssetDependencies.find(handle); it != m_AssetDependencies.end()) + { + result = it->second; + } + } + } + else + { + m_AssetDependencies[handle] = {}; + } + registered = true; + + } + SE_CORE_ASSERT(registered || (GetMetadata(handle).Handle == 0), "asset dependencies are not registered!"); + + return result; + } + + void EditorAssetManager::UpdateDependents(AssetHandle handle) + { + std::unordered_set dependents; + { + std::shared_lock lock(m_AssetDependenciesMutex); + if (auto it = m_AssetDependents.find(handle); it != m_AssetDependents.end()) + dependents = it->second; + } + for (AssetHandle dependent : dependents) + { + if(IsAssetLoaded(dependent)) { + Ref asset = GetAsset(dependent); + if (asset) + { + asset->OnDependencyUpdated(handle); + } + } + } + } + + void EditorAssetManager::SyncWithAssetThread() + { +#if ASYNC_ASSETS + std::vector freshAssets; + + m_AssetThread->RetrieveReadyAssets(freshAssets); + for (auto& alr : freshAssets) + { + SE_CORE_ASSERT(alr.Asset->Handle == alr.Metadata.Handle, "AssetHandle mismatch in AssetLoadResponse"); + m_LoadedAssets[alr.Metadata.Handle] = alr.Asset; + alr.Metadata.Status = AssetStatus::Ready; + alr.Metadata.IsDataLoaded = true; + SetMetadata(alr.Metadata.Handle, alr.Metadata); + } + + m_AssetThread->UpdateLoadedAssetList(m_LoadedAssets); + + // Update dependencies after syncing everything + for (const auto& alr : freshAssets) + { + UpdateDependents(alr.Metadata.Handle); + } +#else + Application::Get().SyncEvents(); +#endif + } + + AssetHandle EditorAssetManager::ImportAsset(const std::filesystem::path& filepath) + { + std::filesystem::path path = GetRelativePath(filepath); + + if (auto handle = GetAssetHandleFromFilePath(path); handle) + { + return handle; + } + + AssetType type = GetAssetTypeFromPath(path); + if (type == AssetType::None) + { + return 0; + } + + AssetMetadata metadata; + metadata.Handle = AssetHandle(); + metadata.FilePath = path; + metadata.Type = type; + + auto absolutePath = GetFileSystemPath(metadata); + metadata.FileLastWriteTime = FileSystem::GetLastWriteTime(absolutePath); + SetMetadata(metadata.Handle, metadata); + + return metadata.Handle; + } + + Ref EditorAssetManager::GetAssetIncludingInvalid(AssetHandle assetHandle) + { + if (auto asset = GetMemoryAsset(assetHandle); asset) + return asset; + + Ref asset = nullptr; + auto metadata = GetMetadata(assetHandle); + if (metadata.IsValid()) + { + if (metadata.IsDataLoaded) + { + asset = m_LoadedAssets[assetHandle]; + } + else + { + if (Application::IsMainThread()) + { + // If we're main thread, we can just try loading the asset as normal + SE_CORE_INFO_TAG("AssetManager", "LOADING ASSET - {}", metadata.FilePath.string()); + if (AssetImporter::TryLoadData(metadata, asset)) + { + auto metadataLoaded = metadata; + metadataLoaded.IsDataLoaded = true; + auto absolutePath = GetFileSystemPath(metadata); + metadataLoaded.FileLastWriteTime = FileSystem::GetLastWriteTime(absolutePath); + m_LoadedAssets[assetHandle] = asset; + SetMetadata(assetHandle, metadataLoaded); + SE_CORE_INFO_TAG("AssetManager", "Finished loading asset {}", metadata.FilePath.string()); + } + else + { + SE_CORE_ERROR_TAG("AssetManager", "Failed to load asset {}", metadata.FilePath.string()); + } + } + else + { + // Not main thread -> ask AssetThread for the asset + // If the asset needs to be loaded, this will load the asset. + // The load will happen on this thread (which is probably asset thread, but occasionally might be audio thread). + // The asset will get synced into main thread at next asset sync point. + asset = m_AssetThread->GetAsset(metadata); + } + } + } + return asset; + } + + void EditorAssetManager::LoadAssetRegistry() + { + SE_CORE_INFO("[AssetManager] Loading Asset Registry"); + + const auto& assetRegistryPath = Project::GetAssetRegistryPath(); + if (!FileSystem::Exists(assetRegistryPath)) + return; + + std::ifstream stream(assetRegistryPath); + SE_CORE_ASSERT(stream); + std::stringstream strStream; + strStream << stream.rdbuf(); + + YAML::Node data = YAML::Load(strStream.str()); + auto handles = data["Assets"]; + if (!handles) + { + SE_CORE_ERROR("[AssetManager] Asset Registry appears to be corrupted!"); + SE_CORE_VERIFY(false); + return; + } + + for (auto entry : handles) + { + std::string filepath = entry["FilePath"].as(); + + AssetMetadata metadata; + metadata.Handle = entry["Handle"].as(); + metadata.FilePath = filepath; + metadata.Type = (AssetType)Utils::AssetTypeFromString(entry["Type"].as()); + + if (metadata.Type == AssetType::None) + continue; + + if (metadata.Type != GetAssetTypeFromPath(filepath)) + { + SE_CORE_WARN_TAG("AssetManager", "Mismatch between stored AssetType and extension type when reading asset registry!"); + metadata.Type = GetAssetTypeFromPath(filepath); + } + + if (!FileSystem::Exists(GetFileSystemPath(metadata))) + { + SE_CORE_WARN("[AssetManager] Missing asset '{0}' detected in registry file, trying to locate...", metadata.FilePath); + + std::string mostLikelyCandidate; + uint32_t bestScore = 0; + + for (auto& pathEntry : std::filesystem::recursive_directory_iterator(Project::GetActiveAssetDirectory())) + { + const std::filesystem::path& path = pathEntry.path(); + + if (path.filename() != metadata.FilePath.filename()) + continue; + + if (bestScore > 0) + SE_CORE_WARN("[AssetManager] Multiple candidates found..."); + + std::vector candiateParts = Utils::SplitString(path.string(), "/\\"); + + uint32_t score = 0; + for (const auto& part : candiateParts) + { + if (filepath.find(part) != std::string::npos) + score++; + } + + SE_CORE_WARN("'{0}' has a score of {1}, best score is {2}", path.string(), score, bestScore); + + if (bestScore > 0 && score == bestScore) + { + // TODO: How do we handle this? + // Probably prompt the user at this point? + } + + if (score <= bestScore) + continue; + + bestScore = score; + mostLikelyCandidate = path.string(); + } + + if (mostLikelyCandidate.empty() && bestScore == 0) + { + SE_CORE_ERROR("[AssetManager] Failed to locate a potential match for '{0}'", metadata.FilePath); + continue; + } + + std::replace(mostLikelyCandidate.begin(), mostLikelyCandidate.end(), '\\', '/'); + metadata.FilePath = std::filesystem::relative(mostLikelyCandidate, Project::GetActive()->GetAssetDirectory()); + SE_CORE_WARN("[AssetManager] Found most likely match '{0}'", metadata.FilePath); + } + + if (metadata.Handle == 0) + { + SE_CORE_WARN("[AssetManager] AssetHandle for {0} is 0, this shouldn't happen.", metadata.FilePath); + continue; + } + + SetMetadata(metadata.Handle, metadata); + } + + SE_CORE_INFO("[AssetManager] Loaded {0} asset entries", m_AssetRegistry.Count()); + } + + void EditorAssetManager::ProcessDirectory(const std::filesystem::path& directoryPath) + { + for (auto entry : std::filesystem::directory_iterator(directoryPath)) + { + if (entry.is_directory()) + ProcessDirectory(entry.path()); + else + ImportAsset(entry.path()); + } + } + + void EditorAssetManager::ReloadAssets() + { + ProcessDirectory(Project::GetActiveAssetDirectory().string()); + WriteRegistryToFile(); + } + + void EditorAssetManager::WriteRegistryToFile() + { + // Sort assets by UUID to make project management easier + struct AssetRegistryEntry + { + std::string FilePath; + AssetType Type; + }; + std::map sortedMap; + for (auto& [filepath, metadata] : m_AssetRegistry) + { + if (!FileSystem::Exists(GetFileSystemPath(metadata))) + continue; + + std::string pathToSerialize = metadata.FilePath.string(); + // NOTE(Yan): if Windows + std::replace(pathToSerialize.begin(), pathToSerialize.end(), '\\', '/'); + sortedMap[metadata.Handle] = { pathToSerialize, metadata.Type }; + } + + SE_CORE_INFO("[AssetManager] serializing asset registry with {0} entries", sortedMap.size()); + + YAML::Emitter out; + out << YAML::BeginMap; + + out << YAML::Key << "Assets" << YAML::BeginSeq; + for (auto& [handle, entry] : sortedMap) + { + out << YAML::BeginMap; + out << YAML::Key << "Handle" << YAML::Value << handle; + out << YAML::Key << "FilePath" << YAML::Value << entry.FilePath; + out << YAML::Key << "Type" << YAML::Value << Utils::AssetTypeToString(entry.Type); + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; + + const std::string& assetRegistryPath = Project::GetAssetRegistryPath().string(); + std::ofstream fout(assetRegistryPath); + fout << out.c_str(); + } + + void EditorAssetManager::OnAssetRenamed(AssetHandle assetHandle, const std::filesystem::path& newFilePath) + { + AssetMetadata metadata = GetMetadata(assetHandle); + if (!metadata.IsValid()) + return; + + metadata.FilePath = GetRelativePath(newFilePath); + SetMetadata(assetHandle, metadata); + WriteRegistryToFile(); + } + + void EditorAssetManager::OnAssetDeleted(AssetHandle assetHandle) + { + RemoveAsset(assetHandle); + WriteRegistryToFile(); + } + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetManager/EditorAssetManager.h b/StarEngine/src/StarEngine/Asset/AssetManager/EditorAssetManager.h new file mode 100644 index 00000000..ddde1294 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetManager/EditorAssetManager.h @@ -0,0 +1,190 @@ +#pragma once + +#include "AssetManagerBase.h" + +#include "StarEngine/Core/Application.h" +#include "StarEngine/Asset/AssetImporter.h" +#include "StarEngine/Asset/AssetRegistry.h" +#include "StarEngine/Asset/AssetSystem/EditorAssetSystem.h" +#include "StarEngine/Core/Events/EditorEvents.h" +#include "StarEngine/Utilities/FileSystem.h" + +#include + +namespace StarEngine { + + class EditorAssetManager : public AssetManagerBase + { + public: + EditorAssetManager(); + virtual ~EditorAssetManager(); + + virtual void Shutdown() override; + + virtual AssetType GetAssetType(AssetHandle assetHandle) override; + virtual Ref GetAsset(AssetHandle assetHandle) override; + virtual AsyncAssetResult GetAssetAsync(AssetHandle assetHandle) override; + + virtual void AddMemoryOnlyAsset(Ref asset) override; + + virtual bool ReloadData(AssetHandle assetHandle) override; + virtual void ReloadDataAsync(AssetHandle assetHandle) override; + virtual bool EnsureCurrent(AssetHandle assetHandle) override; + virtual bool EnsureAllLoadedCurrent() override; + virtual bool IsAssetHandleValid(AssetHandle assetHandle) override { return GetMemoryAsset(assetHandle) || GetMetadata(assetHandle).IsValid(); } + virtual Ref GetMemoryAsset(AssetHandle handle) override; + virtual bool IsAssetLoaded(AssetHandle handle) override; + virtual bool IsAssetValid(AssetHandle handle) override; + virtual bool IsAssetMissing(AssetHandle handle) override; + virtual bool IsMemoryAsset(AssetHandle handle) override; + virtual bool IsPhysicalAsset(AssetHandle handle) override; + virtual void RemoveAsset(AssetHandle handle) override; + + virtual void RegisterDependency(AssetHandle dependency, AssetHandle handle) override; + virtual void DeregisterDependency(AssetHandle dependency, AssetHandle handle) override; + virtual void DeregisterDependencies(AssetHandle handle) override; + virtual std::unordered_set GetDependencies(AssetHandle handle) override; + + virtual void SyncWithAssetThread() override; + + virtual std::unordered_set GetAllAssetsWithType(AssetType type) override; + virtual const std::unordered_map>& GetLoadedAssets() override { return m_LoadedAssets; } + + // ------------- Editor-only ---------------- + + const AssetRegistry& GetAssetRegistry() const { return m_AssetRegistry; } + + // Get all memory-only assets. + // Returned by value so that caller need not hold a lock on m_MemoryAssetsMutex + std::unordered_map> GetMemoryAssets(); + + // note: GetMetadata(AssetHandle) is the ONLY EditorAssetManager function that it is safe to call + // from any thread. + // All other methods on EditorAssetManager are thread-unsafe and should only be called from the main thread. + // SetMetadata() must only be called from main-thread, otherwise it will break safety of all the other + // unsynchronized EditorAssetManager functions. + // + // thread-safe access to metadata + // This function returns an AssetMetadata (specifically not a reference) as with references there is no guarantee + // that the referred to data doesn't get modified (or even destroyed) by another thread + AssetMetadata GetMetadata(AssetHandle handle); + // note: do NOT add non-const version of GetMetadata(). For thread-safety you must modify through SetMetaData() + + // thread-safe modification of metadata + // TODO (0x): don't really need the handle parameter since handle is in metadata anyway + void SetMetadata(AssetHandle handle, const AssetMetadata& metadata); + + AssetHandle ImportAsset(const std::filesystem::path& filepath); + + AssetHandle GetAssetHandleFromFilePath(const std::filesystem::path& filepath); + + AssetType GetAssetTypeFromExtension(const std::string& extension); + std::string GetDefaultExtensionForAssetType(AssetType type); + AssetType GetAssetTypeFromPath(const std::filesystem::path& path); + + std::filesystem::path GetFileSystemPath(AssetHandle handle); + std::filesystem::path GetFileSystemPath(const AssetMetadata& metadata); + std::string GetFileSystemPathString(const AssetMetadata& metadata); + std::filesystem::path GetRelativePath(const std::filesystem::path& filepath); + + bool FileExists(AssetMetadata& metadata) const; + + template + Ref CreateOrReplaceAsset(const std::filesystem::path& path, Args&&... args) + { + static_assert(std::is_base_of::value, "CreateOrReplaceAsset only works for types derived from Asset"); + + // Check if asset for this file already exists. + // If it does, and its the same type we just replace existing asset + // Otherwise we create a whole new asset. + auto relativePath = GetRelativePath(path); + auto handle = GetAssetHandleFromFilePath(relativePath); + AssetMetadata metadata = handle ? GetMetadata(handle) : AssetMetadata{}; + if (metadata.Type != T::GetStaticType()) + { + metadata = {}; + } + + bool replaceAsset = false; + if (metadata.Handle == 0) + { + metadata.Handle = {}; + metadata.FilePath = relativePath; + metadata.Type = T::GetStaticType(); + metadata.IsDataLoaded = true; + SetMetadata(metadata.Handle, metadata); + WriteRegistryToFile(); + } + else + { + replaceAsset = true; + } + + Ref asset = Ref::Create(std::forward(args)...); + asset->Handle = metadata.Handle; + m_LoadedAssets[asset->Handle] = asset; + AssetImporter::Serialize(metadata, asset); + + // Read serialized timestamp + auto absolutePath = GetFileSystemPath(metadata); + metadata.FileLastWriteTime = FileSystem::GetLastWriteTime(absolutePath); + SetMetadata(metadata.Handle, metadata); + + if (replaceAsset) + { + SE_CORE_INFO_TAG("AssetManager", "Replaced asset {}", metadata.FilePath.string()); + UpdateDependents(metadata.Handle); + Application::Get().DispatchEvent(metadata.Handle); + } + + return asset; + } + + void ReplaceLoadedAsset(AssetHandle handle, Ref newAsset) + { + m_LoadedAssets[handle] = newAsset; + } + + + private: + Ref GetAssetIncludingInvalid(AssetHandle assetHandle); + + void LoadAssetRegistry(); + void ProcessDirectory(const std::filesystem::path& directoryPath); + void ReloadAssets(); + void WriteRegistryToFile(); + + void OnAssetRenamed(AssetHandle assetHandle, const std::filesystem::path& newFilePath); + void OnAssetDeleted(AssetHandle assetHandle); + + void UpdateDependents(AssetHandle handle); + + private: + // TODO(Yan): move to AssetSystem + // NOTE (0x): this collection is accessed only from the main thread, and so does not need + // any synchronization + std::unordered_map> m_LoadedAssets; + + // NOTE (0x): this collection is accessed and modified from both the main thread and + // the asset thread, and so requires synchronization + std::unordered_map> m_MemoryAssets; + std::shared_mutex m_MemoryAssetsMutex; + + std::unordered_map> m_AssetDependents; // asset handle -> assets that depend on it. + std::unordered_map> m_AssetDependencies; // asset handle -> assets that it depends on. + std::shared_mutex m_AssetDependenciesMutex; + + Ref m_AssetThread; + + // Asset registry is accessed from multiple threads. + // Access requires synchronization through m_AssetRegistryMutex + // It is _written to_ only by main thread, so reading in main thread can be done without mutex + AssetRegistry m_AssetRegistry; + std::shared_mutex m_AssetRegistryMutex; + + friend class ContentBrowserPanel; + friend class ContentBrowserAsset; + friend class ContentBrowserDirectory; + friend class EditorAssetSystem; + }; +} diff --git a/StarEngine/src/StarEngine/Asset/AssetManager/RuntimeAssetManager.cpp b/StarEngine/src/StarEngine/Asset/AssetManager/RuntimeAssetManager.cpp new file mode 100644 index 00000000..b30b7df2 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetManager/RuntimeAssetManager.cpp @@ -0,0 +1,245 @@ +#include "sepch.h" +#include "RuntimeAssetManager.h" + +#include "StarEngine/Asset/AssetImporter.h" +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Core/Application.h" +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Debug/Profiler.h" + +namespace StarEngine { + + RuntimeAssetManager::RuntimeAssetManager() + { +#if ASYNC_ASSETS + m_AssetThread = Ref::Create(); +#endif + + AssetImporter::Init(); + } + + RuntimeAssetManager::~RuntimeAssetManager() + { + } + + AssetType RuntimeAssetManager::GetAssetType(AssetHandle assetHandle) + { + Ref asset = GetAsset(assetHandle); + if (!asset) + return AssetType::None; + + return asset->GetAssetType(); + } + + Ref RuntimeAssetManager::GetAsset(AssetHandle assetHandle) + { + SE_PROFILE_FUNCTION("RuntimeAssetManager::GetAsset"); + SE_SCOPE_PERF("AssetManager::GetAsset"); + + if (auto asset = GetMemoryAsset(assetHandle); asset) + return asset; + + Ref asset = nullptr; + if(IsAssetLoaded(assetHandle)) + { + asset = m_LoadedAssets[assetHandle]; + } + else + { + if (Application::IsMainThread()) + { + // If we're main thread, we can just loading the asset as normal + asset = m_AssetPack->LoadAsset(m_ActiveScene, assetHandle); + if (asset) + m_LoadedAssets[assetHandle] = asset; + } + else + { + // Not main thread -> ask AssetThread for the asset + // If the asset needs to be loaded, this will load the asset. + // The load will happen on this thread (which is probably asset thread, but occasionally might be audio thread). + // The asset will get synced into main thread at next asset sync point. + asset = m_AssetThread->GetAsset(m_ActiveScene, assetHandle); + } + } + return asset; + } + + AsyncAssetResult RuntimeAssetManager::GetAssetAsync(AssetHandle assetHandle) + { +#if ASYNC_ASSETS + SE_PROFILE_FUNCTION("RuntimeAssetManager::GetAssetAsync"); + SE_SCOPE_PERF("AssetManager::GetAsset"); + + if (auto asset = GetMemoryAsset(assetHandle); asset) + return { asset, true }; + + if(IsAssetLoaded(assetHandle)) + return { m_LoadedAssets.at(assetHandle), true }; + + // Queue load (if not already) and return placeholder + if (!m_PendingAssets.contains(assetHandle)) + { + m_PendingAssets.insert(assetHandle); + RuntimeAssetLoadRequest alr(m_ActiveScene, assetHandle); + m_AssetThread->QueueAssetLoad(alr); + } + + AssetType assetType = m_AssetPack->GetAssetType(m_ActiveScene, assetHandle); + return { AssetManager::GetPlaceholderAsset(assetType), false }; +#else + return { GetAsset(assetHandle), true }; +#endif + } + + void RuntimeAssetManager::AddMemoryOnlyAsset(Ref asset) + { + std::scoped_lock lock(m_MemoryAssetsMutex); + m_MemoryAssets[asset->Handle] = asset; + } + + bool RuntimeAssetManager::ReloadData(AssetHandle assetHandle) + { + Ref asset = m_AssetPack->LoadAsset(m_ActiveScene, assetHandle); + if (asset) + m_LoadedAssets[assetHandle] = asset; + + return asset; + } + + void RuntimeAssetManager::ReloadDataAsync(AssetHandle assetHandle) + { +#if ASYNC_ASSETS + // Queue load (if not already) + if (!m_PendingAssets.contains(assetHandle)) + { + m_PendingAssets.insert(assetHandle); + m_AssetThread->QueueAssetLoad({ m_ActiveScene, assetHandle }); + } +#else + ReloadData(assetHandle); +#endif + } + + bool RuntimeAssetManager::EnsureAllLoadedCurrent() + { + SE_CORE_VERIFY(false); + return false; + } + + bool RuntimeAssetManager::EnsureCurrent(AssetHandle assetHandle) + { + SE_CORE_VERIFY(false); + return false; + } + + bool RuntimeAssetManager::IsAssetHandleValid(AssetHandle assetHandle) + { + if (assetHandle == 0) + return false; + + return GetMemoryAsset(assetHandle) || (m_AssetPack && m_AssetPack->IsAssetHandleValid(assetHandle)); + } + + Ref RuntimeAssetManager::GetMemoryAsset(AssetHandle handle) + { + std::shared_lock lock(m_MemoryAssetsMutex); + if (auto it = m_MemoryAssets.find(handle); it != m_MemoryAssets.end()) + return it->second; + + return nullptr; + } + + bool RuntimeAssetManager::IsAssetLoaded(AssetHandle handle) + { + return m_LoadedAssets.contains(handle); + } + + bool RuntimeAssetManager::IsAssetValid(AssetHandle handle) + { + SE_PROFILE_FUNCTION("RuntimeAssetManager::IsAssetValid"); + SE_SCOPE_PERF("AssetManager::IsAssetValid"); + + return GetAsset(handle) != nullptr; + } + + bool RuntimeAssetManager::IsAssetMissing(AssetHandle handle) + { + return !IsAssetValid(handle); + } + + bool RuntimeAssetManager::IsMemoryAsset(AssetHandle handle) + { + std::scoped_lock lock(m_MemoryAssetsMutex); + return m_MemoryAssets.contains(handle); + } + + bool RuntimeAssetManager::IsPhysicalAsset(AssetHandle handle) + { + return !IsMemoryAsset(handle); + } + + void RuntimeAssetManager::RemoveAsset(AssetHandle handle) + { + { + std::scoped_lock lock(m_MemoryAssetsMutex); + if (m_MemoryAssets.contains(handle)) + m_MemoryAssets.erase(handle); + } + + if (m_LoadedAssets.contains(handle)) + m_LoadedAssets.erase(handle); + + } + + std::unordered_set RuntimeAssetManager::GetAllAssetsWithType(AssetType type) + { + std::unordered_set result; + SE_CORE_VERIFY(false, "Not implemented"); + return result; + } + + void RuntimeAssetManager::SyncWithAssetThread() + { +#if ASYNC_ASSETS + std::vector> freshAssets; + + m_AssetThread->RetrieveReadyAssets(freshAssets); + for (const auto& asset : freshAssets) + { + m_LoadedAssets[asset->Handle] = asset; + SE_CORE_VERIFY(m_PendingAssets.contains(asset->Handle)); + m_PendingAssets.erase(asset->Handle); + } + + m_AssetThread->UpdateLoadedAssetList(m_LoadedAssets); + + // Update dependencies after syncing everything + for (const auto& asset : freshAssets) + { + UpdateDependencies(asset->Handle); + } + +#else + Application::Get().SyncEvents(); +#endif + } + + Ref RuntimeAssetManager::LoadScene(AssetHandle handle) + { + Ref scene = m_AssetPack->LoadScene(handle); + if (scene) + m_ActiveScene = handle; + + return scene; + } + + void RuntimeAssetManager::SetAssetPack(Ref assetPack) + { + m_AssetPack = assetPack; +#if ASYNC_ASSETS + m_AssetThread->SetAssetPack(assetPack); +#endif + } + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetManager/RuntimeAssetManager.h b/StarEngine/src/StarEngine/Asset/AssetManager/RuntimeAssetManager.h new file mode 100644 index 00000000..9a6163d4 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetManager/RuntimeAssetManager.h @@ -0,0 +1,76 @@ +#pragma once + +#include "AssetManagerBase.h" + +#include "StarEngine/Asset/AssetSystem/RuntimeAssetSystem.h" +#include "StarEngine/Serialization/AssetPack.h" + +#include + +namespace StarEngine { + + // AssetPack + class RuntimeAssetManager : public AssetManagerBase + { + public: + RuntimeAssetManager(); + virtual ~RuntimeAssetManager(); + + virtual void Shutdown() override {} + + virtual AssetType GetAssetType(AssetHandle assetHandle) override; + virtual Ref GetAsset(AssetHandle assetHandle) override; + virtual AsyncAssetResult GetAssetAsync(AssetHandle assetHandle) override; + + virtual void AddMemoryOnlyAsset(Ref asset) override; + virtual bool ReloadData(AssetHandle assetHandle) override; + virtual void ReloadDataAsync(AssetHandle assetHandle) override; + virtual bool EnsureAllLoadedCurrent() override; + virtual bool EnsureCurrent(AssetHandle assetHandle) override; + virtual bool IsAssetHandleValid(AssetHandle assetHandle) override; + virtual Ref GetMemoryAsset(AssetHandle handle) override; + virtual bool IsAssetLoaded(AssetHandle handle) override; + virtual bool IsAssetValid(AssetHandle handle) override; + virtual bool IsAssetMissing(AssetHandle handle) override; + virtual bool IsMemoryAsset(AssetHandle handle) override; + virtual bool IsPhysicalAsset(AssetHandle handle) override; + virtual void RemoveAsset(AssetHandle handle) override; + + virtual void RegisterDependency(AssetHandle handle, AssetHandle dependency) override {} + virtual void DeregisterDependency(AssetHandle handle, AssetHandle dependency) override {} + virtual void DeregisterDependencies(AssetHandle handle) override {} + virtual std::unordered_set GetDependencies(AssetHandle handle) override { return {}; } + + virtual void SyncWithAssetThread() override; + + virtual std::unordered_set GetAllAssetsWithType(AssetType type) override; + virtual const std::unordered_map>& GetLoadedAssets() override { return m_LoadedAssets; } + + // ------------- Runtime-only ---------------- + + Ref LoadScene(AssetHandle handle); + + void SetAssetPack(Ref assetPack); + + private: + void UpdateDependencies(AssetHandle handle) {} + + private: + // NOTE (0x): these collections are accessed only from the main thread, and so do not need + // any synchronization + std::unordered_map> m_LoadedAssets; + std::unordered_set m_PendingAssets; + + // NOTE (0x): this collection is accessed and modified from both the main thread and + // the asset thread, and so requires synchronization + std::unordered_map> m_MemoryAssets; + std::shared_mutex m_MemoryAssetsMutex; + + // TODO(Yan): support multiple asset packs maybe? Or at least multiple volumes + Ref m_AssetPack; + AssetHandle m_ActiveScene = 0; + + Ref m_AssetThread; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetManagerBase.cpp b/StarEngine/src/StarEngine/Asset/AssetManagerBase.cpp deleted file mode 100644 index 15c19c38..00000000 --- a/StarEngine/src/StarEngine/Asset/AssetManagerBase.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "sepch.h" -#include "AssetManager.h" - -namespace StarEngine -{ - -} diff --git a/StarEngine/src/StarEngine/Asset/AssetManagerBase.h b/StarEngine/src/StarEngine/Asset/AssetManagerBase.h deleted file mode 100644 index 37d774aa..00000000 --- a/StarEngine/src/StarEngine/Asset/AssetManagerBase.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "Asset.h" - -#include - -namespace StarEngine -{ - using AssetMap = std::map>; - - class AssetManagerBase - { - public: - virtual Ref GetAsset(AssetHandle handle) = 0; - - virtual bool IsAssetHandleValid(AssetHandle handle) const = 0; - virtual bool IsAssetLoaded(AssetHandle handle) const = 0; - virtual AssetType GetAssetType(AssetHandle handle) const = 0; - }; -} diff --git a/StarEngine/src/StarEngine/Asset/AssetMetadata.h b/StarEngine/src/StarEngine/Asset/AssetMetadata.h index d48ec6bc..e93b07d7 100644 --- a/StarEngine/src/StarEngine/Asset/AssetMetadata.h +++ b/StarEngine/src/StarEngine/Asset/AssetMetadata.h @@ -4,14 +4,39 @@ #include -namespace StarEngine -{ +namespace StarEngine { + + enum class AssetStatus + { + None = 0, Ready = 1, Invalid = 2, Loading = 3 + }; + struct AssetMetadata { - AssetType Type = AssetType::None; - std::filesystem::path FilePath = ""; + AssetHandle Handle = 0; + AssetType Type; + std::filesystem::path FilePath; + + AssetStatus Status = AssetStatus::None; + + uint64_t FileLastWriteTime = 0; // TODO: this is the last write time of the file WE LOADED + bool IsDataLoaded = false; + + bool IsValid() const { return Handle != 0; } + }; + + + struct EditorAssetLoadResponse + { + AssetMetadata Metadata; + Ref Asset; + }; + - operator bool() const { return Type != AssetType::None; } + struct RuntimeAssetLoadRequest + { + AssetHandle SceneHandle = 0; + AssetHandle Handle = 0; }; } diff --git a/StarEngine/src/StarEngine/Asset/AssetRegistry.cpp b/StarEngine/src/StarEngine/Asset/AssetRegistry.cpp new file mode 100644 index 00000000..bc722a30 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetRegistry.cpp @@ -0,0 +1,48 @@ +#include "sepch.h" +#include "AssetRegistry.h" + +#include "StarEngine/Core/Application.h" + +namespace StarEngine { + +#define SE_ASSETREGISTRY_LOG 0 +#if SE_ASSETREGISTRY_LOG +#define ASSET_LOG(...) SE_CORE_TRACE_TAG("ASSET", __VA_ARGS__) +#else +#define ASSET_LOG(...) +#endif + + const AssetMetadata& AssetRegistry::Get(const AssetHandle handle) const + { + SE_CORE_ASSERT(m_AssetRegistry.find(handle) != m_AssetRegistry.end()); + ASSET_LOG("Retrieving const handle {}", handle); + return m_AssetRegistry.at(handle); + } + + void AssetRegistry::Set(const AssetHandle handle, const AssetMetadata& metadata) + { + SE_CORE_ASSERT(metadata.Handle == handle); + SE_CORE_ASSERT(handle != 0); + SE_CORE_ASSERT(Application::IsMainThread(), "AssetRegistry::Set() has been called from other than the main thread!"); // Refer comments in EditorAssetManager + m_AssetRegistry[handle] = metadata; + } + + bool AssetRegistry::Contains(const AssetHandle handle) const + { + ASSET_LOG("Contains handle {}", handle); + return m_AssetRegistry.find(handle) != m_AssetRegistry.end(); + } + + size_t AssetRegistry::Remove(const AssetHandle handle) + { + ASSET_LOG("Removing handle", handle); + return m_AssetRegistry.erase(handle); + } + + void AssetRegistry::Clear() + { + ASSET_LOG("Clearing registry"); + m_AssetRegistry.clear(); + } + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetRegistry.h b/StarEngine/src/StarEngine/Asset/AssetRegistry.h new file mode 100644 index 00000000..943c6509 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetRegistry.h @@ -0,0 +1,35 @@ +#pragma once + +#include "StarEngine/Core/Assert.h" +#include "AssetMetadata.h" + +#include +#include + +namespace StarEngine { + + // WARNING: The AssetRegistry is not itself thread-safe, so if accessing AssetRegistry + // via multiple threads, you must take care to provide your own synchronization. + class AssetRegistry + { + public: + + // note: no non-const Get() function. If you need to modify the metadata, use Set(). + // This aids correct usage in a multi-threaded environment. + const AssetMetadata& Get(const AssetHandle handle) const; + void Set(const AssetHandle handle, const AssetMetadata& metadata); + + size_t Count() const { return m_AssetRegistry.size(); } + bool Contains(const AssetHandle handle) const; + size_t Remove(const AssetHandle handle); + void Clear(); + + auto begin() { return m_AssetRegistry.begin(); } + auto end() { return m_AssetRegistry.end(); } + auto begin() const { return m_AssetRegistry.cbegin(); } + auto end() const { return m_AssetRegistry.cend(); } + private: + std::unordered_map m_AssetRegistry; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetSerializer.cpp b/StarEngine/src/StarEngine/Asset/AssetSerializer.cpp new file mode 100644 index 00000000..4e7c6e60 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetSerializer.cpp @@ -0,0 +1,923 @@ +#include "sepch.h" +#include "AssetSerializer.h" + +#include "AssetManager.h" + +#include "StarEngine/Audio/AudioFileUtils.h" +#include "StarEngine/Audio/SoundBank.h" +#include "StarEngine/Audio/ResourceManager.h" + +#include "StarEngine/Scene/Prefab.h" +#include "StarEngine/Scene/SceneSerializer.h" +#include "StarEngine/Scripting/ScriptAsset.h" +#include "StarEngine/Asset/MeshColliderAsset.h" + +#include "StarEngine/Renderer/MaterialAsset.h" +#include "StarEngine/Renderer/Mesh.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Renderer/UI/Font.h" + +#include "StarEngine/Utilities/FileSystem.h" +#include "StarEngine/Utilities/SerializationMacros.h" +#include "StarEngine/Utilities/StringUtils.h" +#include "StarEngine/Utilities/YAMLSerializationHelpers.h" + +#include "StarEngine/Serialization/TextureRuntimeSerializer.h" + +#include "yaml-cpp/yaml.h" + +namespace StarEngine { + + ////////////////////////////////////////////////////////////////////////////////// + // TextureSerializer + ////////////////////////////////////////////////////////////////////////////////// + + bool TextureSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + asset = Texture2D::Create(TextureSpecification(), Project::GetEditorAssetManager()->GetFileSystemPathString(metadata)); + asset->Handle = metadata.Handle; + + bool result = asset.As()->Loaded(); + if (!result) + asset->SetFlag(AssetFlag::Invalid, true); + + return result; + } + + bool TextureSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + outInfo.Offset = stream.GetStreamPosition(); + + auto metadata = Project::GetEditorAssetManager()->GetMetadata(handle); + Ref texture = AssetManager::GetAsset(handle); + outInfo.Size = TextureRuntimeSerializer::SerializeTexture2DToFile(texture, stream); + return true; + } + + Ref TextureSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + return TextureRuntimeSerializer::DeserializeTexture2D(stream); + } + + ////////////////////////////////////////////////////////////////////////////////// + // FontSerializer + ////////////////////////////////////////////////////////////////////////////////// + + bool FontSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + asset = Ref::Create(Project::GetEditorAssetManager()->GetFileSystemPathString(metadata)); + asset->Handle = metadata.Handle; + +#if 0 + // TODO(Yan): we should probably handle fonts not loading correctly + bool result = asset.As()->Loaded(); + if (!result) + asset->SetFlag(AssetFlag::Invalid, true); +#endif + + return true; + } + + bool FontSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + outInfo.Offset = stream.GetStreamPosition(); + + Ref font = AssetManager::GetAsset(handle); + auto path = Project::GetEditorAssetManager()->GetFileSystemPath(handle); + stream.WriteString(font->GetName()); + Buffer fontData = FileSystem::ReadBytes(path); + stream.WriteBuffer(fontData); + + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref FontSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + + std::string name; + stream.ReadString(name); + Buffer fontData; + stream.ReadBuffer(fontData); + + return Ref::Create(name, fontData);; + } + + ////////////////////////////////////////////////////////////////////////////////// + // MaterialAssetSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void MaterialAssetSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref materialAsset = asset.As(); + + std::string yamlString = SerializeToYAML(materialAsset); + + std::ofstream fout(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + fout << yamlString; + } + + bool MaterialAssetSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + Ref materialAsset; + if (!DeserializeFromYAML(GetYAML(metadata), materialAsset, metadata.Handle)) + { + return false; + } + asset = materialAsset; + return true; + } + + void MaterialAssetSerializer::RegisterDependencies(const AssetMetadata& metadata) const + { + RegisterDependenciesFromYAML(GetYAML(metadata), metadata.Handle); + } + + bool MaterialAssetSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + Ref materialAsset = AssetManager::GetAsset(handle); + + std::string yamlString = SerializeToYAML(materialAsset); + outInfo.Offset = stream.GetStreamPosition(); + stream.WriteString(yamlString); + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref MaterialAssetSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + std::string yamlString; + stream.ReadString(yamlString); + + Ref materialAsset; + bool result = DeserializeFromYAML(yamlString, materialAsset, 0); + if (!result) + return nullptr; + + return materialAsset; + } + + std::string MaterialAssetSerializer::SerializeToYAML(Ref materialAsset) const + { + YAML::Emitter out; + out << YAML::BeginMap; // Material + out << YAML::Key << "Material" << YAML::Value; + { + out << YAML::BeginMap; + + // TODO(Yan): this should have shader UUID when that's a thing + // right now only supports PBR or Transparent shaders + Ref transparentShader = Renderer::GetShaderLibrary()->Get("StarPBR_Transparent"); + bool transparent = materialAsset->GetMaterial()->GetShader() == transparentShader; + SE_SERIALIZE_PROPERTY(Transparent, transparent, out); + + SE_SERIALIZE_PROPERTY(AlbedoColor, materialAsset->GetAlbedoColor(), out); + SE_SERIALIZE_PROPERTY(Emission, materialAsset->GetEmission(), out); + if (!transparent) + { + SE_SERIALIZE_PROPERTY(UseNormalMap, materialAsset->IsUsingNormalMap(), out); + SE_SERIALIZE_PROPERTY(Metalness, materialAsset->GetMetalness(), out); + SE_SERIALIZE_PROPERTY(Roughness, materialAsset->GetRoughness(), out); + } + else + { + SE_SERIALIZE_PROPERTY(Transparency, materialAsset->GetTransparency(), out); + } + + { + Ref albedoMap = materialAsset->GetAlbedoMap(); + bool hasAlbedoMap = albedoMap ? !albedoMap.EqualsObject(Renderer::GetWhiteTexture()) : false; + AssetHandle albedoMapHandle = hasAlbedoMap ? albedoMap->Handle : UUID(0); + SE_SERIALIZE_PROPERTY(AlbedoMap, albedoMapHandle, out); + } + if (!transparent) + { + { + Ref normalMap = materialAsset->GetNormalMap(); + bool hasNormalMap = normalMap ? !normalMap.EqualsObject(Renderer::GetWhiteTexture()) : false; + AssetHandle normalMapHandle = hasNormalMap ? normalMap->Handle : UUID(0); + SE_SERIALIZE_PROPERTY(NormalMap, normalMapHandle, out); + } + { + Ref metalnessMap = materialAsset->GetMetalnessMap(); + bool hasMetalnessMap = metalnessMap ? !metalnessMap.EqualsObject(Renderer::GetWhiteTexture()) : false; + AssetHandle metalnessMapHandle = hasMetalnessMap ? metalnessMap->Handle : UUID(0); + SE_SERIALIZE_PROPERTY(MetalnessMap, metalnessMapHandle, out); + } + { + Ref roughnessMap = materialAsset->GetRoughnessMap(); + bool hasRoughnessMap = roughnessMap ? !roughnessMap.EqualsObject(Renderer::GetWhiteTexture()) : false; + AssetHandle roughnessMapHandle = hasRoughnessMap ? roughnessMap->Handle : UUID(0); + SE_SERIALIZE_PROPERTY(RoughnessMap, roughnessMapHandle, out); + } + } + + SE_SERIALIZE_PROPERTY(MaterialFlags, materialAsset->GetMaterial()->GetFlags(), out); + + out << YAML::EndMap; + } + out << YAML::EndMap; // Material + + return std::string(out.c_str()); + } + + std::string MaterialAssetSerializer::GetYAML(const AssetMetadata& metadata) const + { + std::ifstream stream(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + if (!stream.is_open()) + return std::string(); + + std::stringstream strStream; + strStream << stream.rdbuf(); + return strStream.str(); + } + + void MaterialAssetSerializer::RegisterDependenciesFromYAML(const std::string& yamlString, AssetHandle handle) const + { + AssetManager::DeregisterDependencies(handle); + + YAML::Node root = YAML::Load(yamlString); + YAML::Node materialNode = root["Material"]; + + AssetHandle albedoMap, normalMap, metalnessMap, roughnessMap; + SE_DESERIALIZE_PROPERTY(AlbedoMap, albedoMap, materialNode, (AssetHandle)0); + SE_DESERIALIZE_PROPERTY(NormalMap, normalMap, materialNode, (AssetHandle)0); + SE_DESERIALIZE_PROPERTY(MetalnessMap, metalnessMap, materialNode, (AssetHandle)0); + SE_DESERIALIZE_PROPERTY(RoughnessMap, roughnessMap, materialNode, (AssetHandle)0); + + // note: we should always register something, even 0. + AssetManager::RegisterDependency(albedoMap, handle); + AssetManager::RegisterDependency(normalMap, handle); + AssetManager::RegisterDependency(metalnessMap, handle); + AssetManager::RegisterDependency(roughnessMap, handle); + } + + bool MaterialAssetSerializer::DeserializeFromYAML(const std::string& yamlString, Ref& targetMaterialAsset, AssetHandle handle) const + { + RegisterDependenciesFromYAML(yamlString, handle); + + YAML::Node root = YAML::Load(yamlString); + YAML::Node materialNode = root["Material"]; + + bool transparent = false; + SE_DESERIALIZE_PROPERTY(Transparent, transparent, materialNode, false); + + targetMaterialAsset = Ref::Create(transparent); + targetMaterialAsset->Handle = handle; + + SE_DESERIALIZE_PROPERTY(AlbedoColor, targetMaterialAsset->GetAlbedoColor(), materialNode, glm::vec3(0.8f)); + SE_DESERIALIZE_PROPERTY(Emission, targetMaterialAsset->GetEmission(), materialNode, 0.0f); + + if (!transparent) + { + targetMaterialAsset->SetUseNormalMap(materialNode["UseNormalMap"] ? materialNode["UseNormalMap"].as() : false); + SE_DESERIALIZE_PROPERTY(Metalness, targetMaterialAsset->GetMetalness(), materialNode, 0.0f); + SE_DESERIALIZE_PROPERTY(Roughness, targetMaterialAsset->GetRoughness(), materialNode, 0.5f); + } + else + { + SE_DESERIALIZE_PROPERTY(Transparency, targetMaterialAsset->GetTransparency(), materialNode, 1.0f); + } + + AssetHandle albedoMap, normalMap, metalnessMap, roughnessMap; + SE_DESERIALIZE_PROPERTY(AlbedoMap, albedoMap, materialNode, (AssetHandle)0); + if (!transparent) + { + SE_DESERIALIZE_PROPERTY(NormalMap, normalMap, materialNode, (AssetHandle)0); + SE_DESERIALIZE_PROPERTY(MetalnessMap, metalnessMap, materialNode, (AssetHandle)0); + SE_DESERIALIZE_PROPERTY(RoughnessMap, roughnessMap, materialNode, (AssetHandle)0); + } + if (albedoMap) + { + if (AssetManager::IsAssetHandleValid(albedoMap)) + targetMaterialAsset->SetAlbedoMap(albedoMap); + } + if (normalMap) + { + if (AssetManager::IsAssetHandleValid(normalMap)) + targetMaterialAsset->SetNormalMap(normalMap); + } + if (metalnessMap) + { + if (AssetManager::IsAssetHandleValid(metalnessMap)) + targetMaterialAsset->SetMetalnessMap(metalnessMap); + } + if (roughnessMap) + { + if (AssetManager::IsAssetHandleValid(roughnessMap)) + targetMaterialAsset->SetRoughnessMap(roughnessMap); + } + + SE_DESERIALIZE_PROPERTY(MaterialFlags, roughnessMap, materialNode, (AssetHandle)0); + if (materialNode["MaterialFlags"]) + targetMaterialAsset->GetMaterial()->SetFlags(materialNode["MaterialFlags"].as()); + + return true; + } + + ////////////////////////////////////////////////////////////////////////////////// + // EnvironmentSerializer + ////////////////////////////////////////////////////////////////////////////////// + + bool EnvironmentSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + auto [radiance, irradiance] = Renderer::CreateEnvironmentMap(Project::GetEditorAssetManager()->GetFileSystemPathString(metadata)); + + if (!radiance || !irradiance) + return false; + + asset = Ref::Create(radiance, irradiance); + asset->Handle = metadata.Handle; + return true; + } + + bool EnvironmentSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + outInfo.Offset = stream.GetStreamPosition(); + + Ref environment = AssetManager::GetAsset(handle); + uint64_t size = TextureRuntimeSerializer::SerializeToFile(environment->RadianceMap, stream); + size = TextureRuntimeSerializer::SerializeToFile(environment->IrradianceMap, stream); + + // Serialize as just generic TextureCube maybe? + struct EnvironmentMapMetadata + { + uint64_t RadianceMapOffset, RadianceMapSize; + uint64_t IrradianceMapOffset, IrradianceMapSize; + }; + + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref EnvironmentSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + Ref radianceMap = TextureRuntimeSerializer::DeserializeTextureCube(stream); + Ref irradianceMap = TextureRuntimeSerializer::DeserializeTextureCube(stream); + return Ref::Create(radianceMap, irradianceMap); + } + + ////////////////////////////////////////////////////////////////////////////////// + // AudioFileSourceSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void AudioFileSourceSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + + } + + bool AudioFileSourceSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + if (auto opt = AudioFileUtils::GetFileInfo(metadata)) + { + AudioFileUtils::AudioFileInfo info = opt.value(); + asset = Ref::Create(info.Duration, info.SamplingRate, info.BitDepth, info.NumChannels, info.FileSize); + } + else + asset = Ref::Create(); + + asset->Handle = metadata.Handle; + return true; + } + + bool AudioFileSourceSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + //? JP. Disabled as we don't use filepaths for lookup anymore. No idea if this serialization is till needed for the runtime, maybe in the future. +#if 0 + outInfo.Offset = stream.GetStreamPosition(); + + Ref audioFile = AssetManager::GetAsset(handle); + auto path = Project::GetEditorAssetManager()->GetFileSystemPath(handle); + auto relativePath = std::filesystem::relative(path, Project::GetActiveAssetDirectory()); + if (relativePath.empty()) + audioFile->FilePath = path.string(); + else + audioFile->FilePath = relativePath.string(); + + stream.WriteString(audioFile->FilePath); + + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; +#endif + return true; + } + + Ref AudioFileSourceSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + //? JP. Disabled as we don't use filepaths for lookup anymore. No idea if this serialization is till needed for the runtime, maybe in the future. +#if 0 + stream.SetStreamPosition(assetInfo.PackedOffset); + Ref audioFile = Ref::Create(); + stream.ReadString(audioFile->FilePath); +#endif + return Ref::Create(); + } + + ////////////////////////////////////////////////////////////////////////////////// + // SoundConfigSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void SoundConfigSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref soundConfig = asset.As(); + + std::string yamlString = SerializeToYAML(soundConfig); + + std::ofstream fout(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + fout << yamlString; + } + + bool SoundConfigSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + std::ifstream stream(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + if (!stream.is_open()) + return false; + + std::stringstream strStream; + strStream << stream.rdbuf(); + + Ref soundConfig = nullptr; + + // TODO: JP to Yan: Maybe we should make a cleaner intefrace to reuse with other asset types as well? + // Reference to existing asset may be in use somewhere, + // we don't want to invalidate it every time we make a change to the asset. + const bool assetLoaded = Project::GetAssetManager()->IsAssetLoaded(metadata.Handle); //? this check may not be equally correct for Editor and Runtime, or if we change some stuff with asset handling + if (assetLoaded) + { + auto existingAsset = Project::GetAssetManager()->GetAsset(metadata.Handle); + if (existingAsset && existingAsset->GetAssetType() == AssetType::SoundConfig) + soundConfig = existingAsset.As(); + } + else + { + soundConfig = Ref::Create(); + } + + bool success = DeserializeFromYAML(strStream.str(), soundConfig); + if (!success) + return false; + + asset = soundConfig; + asset->Handle = metadata.Handle; + return true; + } + + bool SoundConfigSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + Ref soundConfig = AssetManager::GetAsset(handle); + + std::string yamlString = SerializeToYAML(soundConfig); + outInfo.Offset = stream.GetStreamPosition(); + stream.WriteString(yamlString); + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref SoundConfigSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + std::string yamlString; + stream.ReadString(yamlString); + + Ref soundConfig = Ref::Create(); + bool result = DeserializeFromYAML(yamlString, soundConfig); + if (!result) + return nullptr; + + return soundConfig; + } + + std::string SoundConfigSerializer::SerializeToYAML(Ref soundConfig) const + { + YAML::Emitter out; + + out << YAML::BeginMap; // SoundConfig + if (soundConfig->DataSourceAsset) + { + SE_SERIALIZE_PROPERTY(AssetID, soundConfig->DataSourceAsset, out); + } + + SE_SERIALIZE_PROPERTY(IsLooping, (bool)soundConfig->bLooping, out); + SE_SERIALIZE_PROPERTY(VolumeMultiplier, soundConfig->VolumeMultiplier, out); + SE_SERIALIZE_PROPERTY(PitchMultiplier, soundConfig->PitchMultiplier, out); + SE_SERIALIZE_PROPERTY(MasterReverbSend, soundConfig->MasterReverbSend, out); + SE_SERIALIZE_PROPERTY(LPFilterValue, soundConfig->LPFilterValue, out); + SE_SERIALIZE_PROPERTY(HPFilterValue, soundConfig->HPFilterValue, out); + + // TODO: move Spatialization to its own asset type + out << YAML::Key << "Spatialization"; + out << YAML::BeginMap; // Spatialization + + auto& spatialConfig = soundConfig->Spatialization; + SE_SERIALIZE_PROPERTY(Enabled, soundConfig->bSpatializationEnabled, out); + SE_SERIALIZE_PROPERTY(AttenuationModel, (int)spatialConfig->AttenuationMod, out); + SE_SERIALIZE_PROPERTY(MinGain, spatialConfig->MinGain, out); + SE_SERIALIZE_PROPERTY(MaxGain, spatialConfig->MaxGain, out); + SE_SERIALIZE_PROPERTY(MinDistance, spatialConfig->MinDistance, out); + SE_SERIALIZE_PROPERTY(MaxDistance, spatialConfig->MaxDistance, out); + SE_SERIALIZE_PROPERTY(ConeInnerAngle, spatialConfig->ConeInnerAngleInRadians, out); + SE_SERIALIZE_PROPERTY(ConeOuterAngle, spatialConfig->ConeOuterAngleInRadians, out); + SE_SERIALIZE_PROPERTY(ConeOuterGain, spatialConfig->ConeOuterGain, out); + SE_SERIALIZE_PROPERTY(DopplerFactor, spatialConfig->DopplerFactor, out); + SE_SERIALIZE_PROPERTY(Rollor, spatialConfig->Rolloff, out); + SE_SERIALIZE_PROPERTY(AirAbsorptionEnabled, spatialConfig->bAirAbsorptionEnabled, out); + SE_SERIALIZE_PROPERTY(SpreadFromSourceSize, spatialConfig->bSpreadFromSourceSize, out); + SE_SERIALIZE_PROPERTY(SourceSize, spatialConfig->SourceSize, out); + SE_SERIALIZE_PROPERTY(Spread, spatialConfig->Spread, out); + SE_SERIALIZE_PROPERTY(Focus, spatialConfig->Focus, out); + + out << YAML::EndMap; // Spatialization + out << YAML::EndMap; // SoundConfig + + return std::string(out.c_str()); + } + + bool SoundConfigSerializer::DeserializeFromYAML(const std::string& yamlString, Ref targetSoundConfig) const + { + YAML::Node data = YAML::Load(yamlString); + + AssetHandle assetHandle = data["AssetID"] ? data["AssetID"].as() : 0; + + bool valid = true; + if (Application::IsRuntime()) + { + AssetType type = AssetManager::GetAssetType(assetHandle); + + if (type == AssetType::Audio) + { + // If actual audio file, check if it exists in SoundBank + Ref soundBank = MiniAudioEngine::Get().GetResourceManager()->GetSoundBank(); + SE_CORE_VERIFY(soundBank, "SoundBank is not loaded!"); + + valid = soundBank->Contains(assetHandle); + } + } + else + { + valid = AssetManager::IsAssetHandleValid(assetHandle); + } + + if (valid) + { + targetSoundConfig->DataSourceAsset = assetHandle; + } + else + { + if (Application::IsRuntime()) + { + SE_CORE_ERROR("Tried to load invalid audio source asset (UUID {0}) in SoundConfig: {1}", + assetHandle, targetSoundConfig->Handle); + } + else + { + SE_CORE_ERROR("Tried to load invalid audio source asset (UUID {0}) in SoundConfig: {1}", + assetHandle, Project::GetEditorAssetManager()->GetFileSystemPath(targetSoundConfig->Handle).string()); + } + } + + SE_DESERIALIZE_PROPERTY(IsLooping, targetSoundConfig->bLooping, data, false); + SE_DESERIALIZE_PROPERTY(VolumeMultiplier, targetSoundConfig->VolumeMultiplier, data, 1.0f); + SE_DESERIALIZE_PROPERTY(PitchMultiplier, targetSoundConfig->PitchMultiplier, data, 1.0f); + SE_DESERIALIZE_PROPERTY(MasterReverbSend, targetSoundConfig->MasterReverbSend, data, 0.0f); + SE_DESERIALIZE_PROPERTY(LPFilterValue, targetSoundConfig->LPFilterValue, data, 20000.0f); + SE_DESERIALIZE_PROPERTY(HPFilterValue, targetSoundConfig->HPFilterValue, data, 0.0f); + + auto spConfigData = data["Spatialization"]; + if (spConfigData) + { + targetSoundConfig->bSpatializationEnabled = spConfigData["Enabled"] ? spConfigData["Enabled"].as() : false; + + auto& spatialConfig = targetSoundConfig->Spatialization; + + SE_DESERIALIZE_PROPERTY(Enabled, targetSoundConfig->bSpatializationEnabled, spConfigData, false); + spatialConfig->AttenuationMod = spConfigData["AttenuationModel"] ? static_cast(spConfigData["AttenuationModel"].as()) + : AttenuationModel::Inverse; + + SE_DESERIALIZE_PROPERTY(MinGain, spatialConfig->MinGain, spConfigData, 0.0f); + SE_DESERIALIZE_PROPERTY(MaxGain, spatialConfig->MaxGain, spConfigData, 1.0f); + SE_DESERIALIZE_PROPERTY(MinDistance, spatialConfig->MinDistance, spConfigData, 1.0f); + SE_DESERIALIZE_PROPERTY(MaxDistance, spatialConfig->MaxDistance, spConfigData, 1000.0f); + SE_DESERIALIZE_PROPERTY(ConeInnerAngle, spatialConfig->ConeInnerAngleInRadians, spConfigData, 6.283185f); + SE_DESERIALIZE_PROPERTY(ConeOuterAngle, spatialConfig->ConeOuterAngleInRadians, spConfigData, 6.283185f); + SE_DESERIALIZE_PROPERTY(ConeOuterGain, spatialConfig->ConeOuterGain, spConfigData, 0.0f); + SE_DESERIALIZE_PROPERTY(DopplerFactor, spatialConfig->DopplerFactor, spConfigData, 1.0f); + SE_DESERIALIZE_PROPERTY(Rollor, spatialConfig->Rolloff, spConfigData, 1.0f); + SE_DESERIALIZE_PROPERTY(AirAbsorptionEnabled, spatialConfig->bAirAbsorptionEnabled, spConfigData, true); + SE_DESERIALIZE_PROPERTY(SpreadFromSourceSize, spatialConfig->bSpreadFromSourceSize, spConfigData, true); + SE_DESERIALIZE_PROPERTY(SourceSize, spatialConfig->SourceSize, spConfigData, 1.0f); + SE_DESERIALIZE_PROPERTY(Spread, spatialConfig->Spread, spConfigData, 1.0f); + SE_DESERIALIZE_PROPERTY(Focus, spatialConfig->Focus, spConfigData, 1.0f); + } + + return true; + } + + ////////////////////////////////////////////////////////////////////////////////// + // PrefabSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void PrefabSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref prefab = asset.As(); + + std::string yamlString = SerializeToYAML(prefab); + + std::ofstream fout(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + fout << yamlString; + } + + bool PrefabSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + std::ifstream stream(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + if (!stream.is_open()) + return false; + + std::stringstream strStream; + strStream << stream.rdbuf(); + + Ref prefab = Ref::Create(); + bool success = DeserializeFromYAML(strStream.str(), prefab); + if (!success) + return false; + + asset = prefab; + asset->Handle = metadata.Handle; + return true; + } + + bool PrefabSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + Ref prefab = AssetManager::GetAsset(handle); + + std::string yamlString = SerializeToYAML(prefab); + outInfo.Offset = stream.GetStreamPosition(); + stream.WriteString(yamlString); + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref PrefabSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + std::string yamlString; + stream.ReadString(yamlString); + + Ref prefab = Ref::Create(); + bool result = DeserializeFromYAML(yamlString, prefab); + if (!result) + return nullptr; + + return prefab; + } + + std::string PrefabSerializer::SerializeToYAML(Ref prefab) const + { + YAML::Emitter out; + + out << YAML::BeginMap; + out << YAML::Key << "Prefab"; + out << YAML::Value << YAML::BeginSeq; + + prefab->m_Scene->m_Registry.each([&](auto entityID) + { + Entity entity = { entityID, prefab->m_Scene.Raw() }; + if (!entity || !entity.HasComponent()) + return; + + SceneSerializer::SerializeEntity(out, entity, prefab->m_Scene); + }); + + out << YAML::EndSeq; + out << YAML::EndMap; + + return std::string(out.c_str()); + } + + bool PrefabSerializer::DeserializeFromYAML(const std::string& yamlString, Ref prefab) const + { + YAML::Node data = YAML::Load(yamlString); + if (!data["Prefab"]) + return false; + + YAML::Node prefabNode = data["Prefab"]; + SceneSerializer::DeserializeEntities(prefabNode, prefab->m_Scene); + return true; + } + + ////////////////////////////////////////////////////////////////////////////////// + // SceneAssetSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void SceneAssetSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + SceneSerializer serializer(asset.As()); + serializer.Serialize(Project::GetEditorAssetManager()->GetFileSystemPath(metadata).string()); + } + + bool SceneAssetSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + asset = Ref::Create("SceneAsset", false, false); + asset->Handle = metadata.Handle; + return true; + } + + bool SceneAssetSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + Ref scene = Ref::Create("AssetPackTemp", true, false); + const auto& metadata = Project::GetEditorAssetManager()->GetMetadata(handle); + SceneSerializer serializer(scene); + if (serializer.Deserialize(Project::GetActiveAssetDirectory() / metadata.FilePath)) + { + return serializer.SerializeToAssetPack(stream, outInfo); + } + return false; + } + + Ref SceneAssetSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + SE_CORE_VERIFY(false); // Not implemented + return nullptr; + } + + Ref SceneAssetSerializer::DeserializeSceneFromAssetPack(FileStreamReader& stream, const AssetPackFile::SceneInfo& sceneInfo) const + { + Ref scene = Ref::Create(); + SceneSerializer serializer(scene); + if (serializer.DeserializeFromAssetPack(stream, sceneInfo)) + return scene; + + return nullptr; + } + + ////////////////////////////////////////////////////////////////////////////////// + // MeshColliderSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void MeshColliderSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref meshCollider = asset.As(); + + std::string yamlString = SerializeToYAML(meshCollider); + + std::ofstream fout(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + fout << yamlString; + } + + bool MeshColliderSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + std::ifstream stream(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + if (!stream.is_open()) + return false; + + std::stringstream strStream; + strStream << stream.rdbuf(); + + if (strStream.rdbuf()->in_avail() == 0) + return false; + + Ref meshCollider = Ref::Create(); + bool result = DeserializeFromYAML(strStream.str(), meshCollider); + if (!result) + return false; + + asset = meshCollider; + asset->Handle = metadata.Handle; + return true; + } + + bool MeshColliderSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + Ref meshCollider = AssetManager::GetAsset(handle); + + std::string yamlString = SerializeToYAML(meshCollider); + outInfo.Offset = stream.GetStreamPosition(); + stream.WriteString(yamlString); + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref MeshColliderSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + std::string yamlString; + stream.ReadString(yamlString); + + Ref meshCollider = Ref::Create(); + bool result = DeserializeFromYAML(yamlString, meshCollider); + if (!result) + return nullptr; + + return meshCollider; + } + + std::string MeshColliderSerializer::SerializeToYAML(Ref meshCollider) const + { + YAML::Emitter out; + + out << YAML::BeginMap; + out << YAML::Key << "ColliderMesh" << YAML::Value << meshCollider->ColliderMesh; + out << YAML::Key << "EnableVertexWelding" << YAML::Value << meshCollider->EnableVertexWelding; + out << YAML::Key << "VertexWeldTolerance" << YAML::Value << meshCollider->VertexWeldTolerance; + out << YAML::Key << "FlipNormals" << YAML::Value << meshCollider->FlipNormals; + out << YAML::Key << "CheckZeroAreaTriangles" << YAML::Value << meshCollider->CheckZeroAreaTriangles; + out << YAML::Key << "AreaTestEpsilon" << YAML::Value << meshCollider->AreaTestEpsilon; + out << YAML::Key << "ShiftVerticesToOrigin" << YAML::Value << meshCollider->ShiftVerticesToOrigin; + out << YAML::Key << "AlwaysShareShape" << YAML::Value << meshCollider->AlwaysShareShape; + out << YAML::Key << "CollisionComplexity" << YAML::Value << (uint8_t)meshCollider->CollisionComplexity; + out << YAML::Key << "ColliderScale" << YAML::Value << meshCollider->ColliderScale; + out << YAML::Key << "PreviewScale" << YAML::Value << meshCollider->PreviewScale; + + out << YAML::Key << "ColliderMaterial" << YAML::BeginMap; + out << YAML::Key << "Friction" << YAML::Value << meshCollider->Material.Friction; + out << YAML::Key << "Restitution" << YAML::Value << meshCollider->Material.Restitution; + out << YAML::EndMap; + + out << YAML::EndMap; + + return std::string(out.c_str()); + } + + bool MeshColliderSerializer::DeserializeFromYAML(const std::string& yamlString, Ref targetMeshCollider) const + { + YAML::Node data = YAML::Load(yamlString); + + targetMeshCollider->ColliderMesh = data["ColliderMesh"].as(0); + targetMeshCollider->Material.Friction = data["ColliderMaterial"]["Friction"].as(0.1f); + targetMeshCollider->Material.Restitution = data["ColliderMaterial"]["Restitution"].as(0.05f); + targetMeshCollider->EnableVertexWelding = data["EnableVertexWelding"].as(true); + targetMeshCollider->VertexWeldTolerance = glm::clamp(data["VertexWeldTolerance"].as(0.1f), 0.05f, 1.0f); + targetMeshCollider->FlipNormals = data["FlipNormals"].as(false); + targetMeshCollider->CheckZeroAreaTriangles = data["CheckZeroAreaTriangles"].as(false); + targetMeshCollider->AreaTestEpsilon = glm::max(0.06f, data["AreaTestEpsilon"].as(0.06f)); + targetMeshCollider->ShiftVerticesToOrigin = data["ShiftVerticesToOrigin"].as(false); + targetMeshCollider->AlwaysShareShape = data["AlwaysShareShape"].as(false); + targetMeshCollider->CollisionComplexity = (ECollisionComplexity)data["CollisionComplexity"].as(0); + targetMeshCollider->ColliderScale = data["ColliderScale"].as(glm::vec3(1.0f)); + targetMeshCollider->PreviewScale = data["PreviewScale"].as(glm::vec3(1.0f)); + + return true; + } + + ////////////////////////////////////////////////////////////////////////////////// + // ScriptFileSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void ScriptFileSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + std::ofstream stream(Project::GetEditorAssetManager()->GetFileSystemPath(metadata)); + SE_CORE_VERIFY(stream.is_open()); + + std::ifstream templateStream("Resources/Templates/NewClassTemplate.cs"); + SE_CORE_VERIFY(templateStream.is_open()); + + std::stringstream templateStrStream; + templateStrStream << templateStream.rdbuf(); + std::string templateString = templateStrStream.str(); + + templateStream.close(); + + auto replaceTemplateToken = [&templateString](const char* token, const std::string& value) + { + size_t pos = 0; + while ((pos = templateString.find(token, pos)) != std::string::npos) + { + templateString.replace(pos, strlen(token), value); + pos += strlen(token); + } + }; + + auto scriptFileAsset = asset.As(); + replaceTemplateToken("$NAMESPACE_NAME$", scriptFileAsset->GetClassNamespace()); + replaceTemplateToken("$CLASS_NAME$", scriptFileAsset->GetClassName()); + + stream << templateString; + stream.close(); + } + + bool ScriptFileSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + asset = Ref::Create(); + asset->Handle = metadata.Handle; + return true; + } + + bool ScriptFileSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + return true; + } + + Ref ScriptFileSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + SE_CORE_VERIFY(false); // Not implemented + return nullptr; + } + + void AssetSerializer::RegisterDependencies(const AssetMetadata& metadata) const + { + AssetManager::RegisterDependency(0, metadata.Handle); + } + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetSerializer.h b/StarEngine/src/StarEngine/Asset/AssetSerializer.h new file mode 100644 index 00000000..620b9a69 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetSerializer.h @@ -0,0 +1,151 @@ +#pragma once + +#include "AssetMetadata.h" +#include "MeshColliderAsset.h" + +#include "StarEngine/Serialization/FileStream.h" +#include "StarEngine/Serialization/AssetPackFile.h" + +namespace StarEngine { + + class MaterialAsset; + class MeshColliderAsset; + class PhysicsMaterial; + class Prefab; + class Scene; + struct SoundConfig; + + struct AssetSerializationInfo + { + uint64_t Offset = 0; + uint64_t Size = 0; + }; + + class AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const = 0; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const = 0; + virtual void RegisterDependencies(const AssetMetadata& metadata) const; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const = 0; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const = 0; + }; + + class TextureSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override{} + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + + class FontSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override {} + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + + class MaterialAssetSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + virtual void RegisterDependencies(const AssetMetadata& metadata) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + private: + std::string SerializeToYAML(Ref materialAsset) const; + std::string GetYAML(const AssetMetadata& metadata) const; + void RegisterDependenciesFromYAML(const std::string& yamlString, AssetHandle handle) const; + bool DeserializeFromYAML(const std::string& yamlString, Ref& targetMaterialAsset, AssetHandle handle) const; + }; + + class EnvironmentSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override{} + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + + class AudioFileSourceSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + + class SoundConfigSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + private: + std::string SerializeToYAML(Ref soundConfig) const; + bool DeserializeFromYAML(const std::string& yamlString, Ref targetSoundConfig) const; + }; + + class PrefabSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + private: + std::string SerializeToYAML(Ref prefab) const; + bool DeserializeFromYAML(const std::string& yamlString, Ref prefab) const; + }; + + class SceneAssetSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + Ref DeserializeSceneFromAssetPack(FileStreamReader& stream, const AssetPackFile::SceneInfo& sceneInfo) const; + }; + + class MeshColliderSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + private: + std::string SerializeToYAML(Ref meshCollider) const; + bool DeserializeFromYAML(const std::string& yamlString, Ref targetMeshCollider) const; + }; + + class ScriptFileSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetSystem/EditorAssetSystem.cpp b/StarEngine/src/StarEngine/Asset/AssetSystem/EditorAssetSystem.cpp new file mode 100644 index 00000000..955c8a26 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetSystem/EditorAssetSystem.cpp @@ -0,0 +1,233 @@ +#include "sepch.h" +#include "EditorAssetSystem.h" + +#include "StarEngine/Asset/AssetImporter.h" +#include "StarEngine/Project/Project.h" +#include "StarEngine/Core/Application.h" +#include "StarEngine/Core/Events/EditorEvents.h" + +#include "StarEngine/Debug/Profiler.h" + +namespace StarEngine { + + EditorAssetSystem::EditorAssetSystem() + : m_Thread("Asset Thread") + { + m_Thread.Dispatch([this]() { AssetThreadFunc(); }); + } + + EditorAssetSystem::~EditorAssetSystem() + { + StopAndWait(); + } + + void EditorAssetSystem::Stop() + { + m_Running = false; + m_AssetLoadingQueueCV.notify_one(); + } + + void EditorAssetSystem::StopAndWait() + { + Stop(); + m_Thread.Join(); + } + + void EditorAssetSystem::AssetMonitorUpdate() + { + Timer timer; + EnsureAllLoadedCurrent(); + m_AssetUpdatePerf = timer.ElapsedMillis(); + } + + void EditorAssetSystem::AssetThreadFunc() + { + SE_PROFILE_THREAD("Asset Thread"); + + while (m_Running) + { + SE_PROFILE_SCOPE("Asset Thread Queue"); + + AssetMonitorUpdate(); + + bool queueEmptyOrStop = false; + while (!queueEmptyOrStop) + { + AssetMetadata metadata; + { + std::scoped_lock lock(m_AssetLoadingQueueMutex); + if (m_AssetLoadingQueue.empty() || !m_Running) + { + queueEmptyOrStop = true; + } + else + { + metadata = m_AssetLoadingQueue.front(); + m_AssetLoadingQueue.pop(); + } + } + + // If queueEmptyOrStop then metadata will be invalid (Handle == 0) + // We check metadata here (instead of just breaking straight away on queueEmptyOrStop) + // to deal with the edge case that other thread might queue requests for invalid assets. + // This way, we just pop those requests and ignore them. + if (metadata.IsValid()) + { + TryLoadData(metadata); + } + } + + std::unique_lock lock(m_AssetLoadingQueueMutex); + // need to check conditions again, since other thread could have changed them between releasing the lock (in the while loop above) + // and re-acquiring the lock here + if (m_AssetLoadingQueue.empty() && m_Running) + { + // need to wake periodically (here 100ms) so that AssetMonitorUpdate() is called regularly to check for updated file timestamps + // (which kinda makes condition variable redundant. may as well just sleep(100ms)) + m_AssetLoadingQueueCV.wait_for(lock, std::chrono::milliseconds(100), [this] { + return !m_Running || !m_AssetLoadingQueue.empty(); + }); + } + } + } + + void EditorAssetSystem::QueueAssetLoad(const AssetMetadata& request) + { + { + std::scoped_lock lock(m_AssetLoadingQueueMutex); + m_AssetLoadingQueue.push(request); + } + m_AssetLoadingQueueCV.notify_one(); + } + + Ref EditorAssetSystem::GetAsset(const AssetMetadata& request) + { + { + std::scoped_lock lock(m_AMLoadedAssetsMutex); + if (auto it = m_AMLoadedAssets.find(request.Handle); it != m_AMLoadedAssets.end()) + return it->second; + } + return TryLoadData(request); + } + + bool EditorAssetSystem::RetrieveReadyAssets(std::vector& outAssetList) + { + std::vector loadedAssets; + { + std::scoped_lock lock(m_LoadedAssetsMutex); + std::swap(loadedAssets, m_LoadedAssets); + + // Now that we've sync'd assets, any events that were dispatched from TryLoadData() are safe to be processed + Application::Get().SyncEvents(); + } + + if (loadedAssets.empty()) + return false; + + outAssetList = loadedAssets; + return true; + } + + void EditorAssetSystem::UpdateLoadedAssetList(const std::unordered_map>& loadedAssets) + { + std::scoped_lock lock(m_AMLoadedAssetsMutex); + m_AMLoadedAssets = loadedAssets; + } + + std::filesystem::path EditorAssetSystem::GetFileSystemPath(const AssetMetadata& metadata) + { + // TODO (0x): This is not safe. Project asset directory can be modified by other threads + return Project::GetActiveAssetDirectory() / metadata.FilePath; + } + + void EditorAssetSystem::EnsureAllLoadedCurrent() + { + SE_PROFILE_FUNCTION("EditorAssetSystem::EnsureAllLoadedCurrent"); + + // This is a long time to hold a lock. + // However, copying the list of assets to iterate (so that we could then release the lock before iterating) is also expensive, so hard to tell which is better. + std::scoped_lock lock(m_AMLoadedAssetsMutex); + for (const auto& [handle, asset] : m_AMLoadedAssets) + { + EnsureCurrent(handle); + } + } + + void EditorAssetSystem::EnsureCurrent(AssetHandle assetHandle) + { + auto metadata = Project::GetEditorAssetManager()->GetMetadata(assetHandle); + + // other thread could have deleted the asset since our asset list was last sync'd + if(!metadata.IsValid()) return; + + auto absolutePath = GetFileSystemPath(metadata); + + if (!FileSystem::Exists(absolutePath)) + return; + + uint64_t actualLastWriteTime = FileSystem::GetLastWriteTime(absolutePath); + uint64_t recordedLastWriteTime = metadata.FileLastWriteTime; + + if (actualLastWriteTime == recordedLastWriteTime) + return; + + if (actualLastWriteTime == 0 || recordedLastWriteTime == 0) + return; + + // Queue here rather than TryLoad(), as we are holding a lock (in EnsureAllLoadedCurrent()) and want this function to return as quickly as possible + return QueueAssetLoad(metadata); + } + + +// // TODO (0x): delete once dependent assets are handled properly using asset dependencies +// bool EditorAssetSystem::ReloadData(AssetHandle handle) +// { +// auto metadata = Project::GetEditorAssetManager()->GetMetadata(handle); +// return metadata.IsValid() && ReloadData(metadata); +// } + + + Ref EditorAssetSystem::TryLoadData(AssetMetadata metadata) + { + Ref asset; + if (!metadata.IsValid()) + { + SE_CORE_ERROR("Trying to load invalid asset"); + return asset; + } + + SE_CORE_INFO_TAG("AssetSystem", "{}LOADING ASSET - {}", metadata.IsDataLoaded? "RE" : "", metadata.FilePath.string()); + + // ASSUMPTION: AssetImporter serializers are immutable and re-entrant + if (AssetImporter::TryLoadData(metadata, asset)) + { + metadata.IsDataLoaded = true; + auto absolutePath = GetFileSystemPath(metadata); + + // Note (0x): There's a small hole here. Other thread could start writing to asset's file in the exact instant that TryLoadData() has finished with it. + // GetLastWriteTime() then blocks until the write has finished, but now we have a new write time - not the one that was relevent for TryLoadData() + // To resolve this, you bascially need to lock the metadata until both the TryLoadData() _and_ the GetLastWriteTime() have completed. + // Or you need to update the last write time while you still have the file locked during TryLoadData() + metadata.FileLastWriteTime = FileSystem::GetLastWriteTime(absolutePath); + { + std::scoped_lock lock(m_LoadedAssetsMutex); + m_LoadedAssets.emplace_back(metadata, asset); + + // be careful: + // 1) DispatchEvent() is only thread-safe when DispatchImmediately is false. + // 2) Events must be handled carefully so that we are sure that the assets have been synched back to main thread _before_ this event is processed. + // That's why we are dispatching event while we hold a lock on m_LoadedAssetsMutex (see RetrieveReadyAssets()) + Application::Get().DispatchEvent(metadata.Handle); + } + + SE_CORE_INFO_TAG("AssetSystem", "Finished loading asset {}", metadata.FilePath.string()); + } + else + { + SE_CORE_ERROR_TAG("AssetSystem", "Failed to load asset {} ({})", metadata.Handle, metadata.FilePath); + } + + return asset; + } + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetSystem/EditorAssetSystem.h b/StarEngine/src/StarEngine/Asset/AssetSystem/EditorAssetSystem.h new file mode 100644 index 00000000..51c0d4f2 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetSystem/EditorAssetSystem.h @@ -0,0 +1,67 @@ +#pragma once + +#include "StarEngine/Asset/AssetMetadata.h" +#include "StarEngine/Core/Thread.h" + +#include +#include +#include + +namespace StarEngine { + + class EditorAssetSystem : public RefCounted + { + public: + EditorAssetSystem(); + ~EditorAssetSystem(); + + // Queue an asset to be loaded on asset thread later + void QueueAssetLoad(const AssetMetadata& request); + + // Get an asset immediately (on asset thread). + // If the asset needs to be loaded, it will be loaded into "ready assets" and transfered back to main thread + // at next asset sync. + Ref GetAsset(const AssetMetadata& request); + + // Retrieve assets that have been loaded (from and earlier request) + bool RetrieveReadyAssets(std::vector& outAssetList); + + // Replace the currently loaded asset collection with the given loadedAssets. + // This effectively takes a "thread local" snapshot of the asset manager's loaded assets. + void UpdateLoadedAssetList(const std::unordered_map>& loadedAssets); + + void Stop(); + void StopAndWait(); + + // Monitor for updated assets, and if any are found queue them for reload + void AssetMonitorUpdate(); + + private: + // The asset thread's mainline + void AssetThreadFunc(); + + std::filesystem::path GetFileSystemPath(const AssetMetadata& metadata); + + void EnsureAllLoadedCurrent(); + void EnsureCurrent(AssetHandle assetHandle); + Ref TryLoadData(AssetMetadata metadata); + + private: + Thread m_Thread; + std::atomic m_Running = true; // not false. This ensures that if Stop() is called after the thread is dispatched but before it actually starts running, then the thread is correctly stopped. + + std::queue m_AssetLoadingQueue; + std::mutex m_AssetLoadingQueueMutex; + std::condition_variable m_AssetLoadingQueueCV; + + std::vector m_LoadedAssets; // Assets that have been loaded asynchronously and are waiting for sync back to Asset Manager + std::mutex m_LoadedAssetsMutex; + + std::unordered_map> m_AMLoadedAssets; // All currently loaded assets (synced from Asset Manager) + std::mutex m_AMLoadedAssetsMutex; + + // Asset Monitoring + float m_AssetUpdatePerf = 0.0f; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetSystem/RuntimeAssetSystem.cpp b/StarEngine/src/StarEngine/Asset/AssetSystem/RuntimeAssetSystem.cpp new file mode 100644 index 00000000..3fc8d560 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetSystem/RuntimeAssetSystem.cpp @@ -0,0 +1,133 @@ +#include "sepch.h" +#include "RuntimeAssetSystem.h" + +#include "StarEngine/Debug/Profiler.h" +#include "StarEngine/Project/Project.h" + +namespace StarEngine { + + RuntimeAssetSystem::RuntimeAssetSystem() + : m_Thread("Asset Thread") + { + m_Thread.Dispatch([this]() { AssetThreadFunc(); }); + } + + RuntimeAssetSystem::~RuntimeAssetSystem() + { + StopAndWait(); + } + + void RuntimeAssetSystem::Stop() + { + m_Running = false; + m_AssetLoadingQueueCV.notify_one(); + } + + void RuntimeAssetSystem::StopAndWait() + { + Stop(); + m_Thread.Join(); + } + + void RuntimeAssetSystem::AssetThreadFunc() + { + SE_PROFILE_THREAD("Asset Thread"); + + while (m_Running) + { + SE_PROFILE_SCOPE("Asset Thread Queue"); + + // Go through queue and see what needs loading + bool queueEmptyOrStop = false; + while (!queueEmptyOrStop) + { + RuntimeAssetLoadRequest alr; + { + std::scoped_lock lock(m_AssetLoadingQueueMutex); + if (m_AssetLoadingQueue.empty() || !m_Running) + { + queueEmptyOrStop = true; + } + else + { + alr = m_AssetLoadingQueue.front(); + m_AssetLoadingQueue.pop(); + } + } + + // If queueEmptyOrStop then request will be invalid (Handle == 0) + // We check handles here (instead of just breaking straight away on queueEmptyOrStop) + // to deal with the edge case that other thread might queue requests for invalid assets. + // This way, we just pop those requests and ignore them. + if (alr.Handle != 0 && alr.SceneHandle != 0) + { + TryLoadData(alr); + } + } + + std::unique_lock lock(m_AssetLoadingQueueMutex); + // need to check conditions again, since other thread could have changed them between releasing the lock (in the while loop above) + // and re-acquiring the lock here + if (m_AssetLoadingQueue.empty() && m_Running) + { + m_AssetLoadingQueueCV.wait(lock, [this] { + return !m_Running || !m_AssetLoadingQueue.empty(); + }); + } + } + } + + Ref RuntimeAssetSystem::TryLoadData(const RuntimeAssetLoadRequest& alr) + { + SE_CORE_INFO_TAG("AssetSystem", "Loading asset {}", alr.Handle); + Ref asset; + if (asset = m_AssetPack->LoadAsset(alr.SceneHandle, alr.Handle); asset) + { + std::scoped_lock lock(m_LoadedAssetsMutex); + m_LoadedAssets.emplace_back(asset); + SE_CORE_INFO_TAG("AssetSystem", "Finished loading asset {}", alr.Handle); + } + else + { + SE_CORE_ERROR_TAG("AssetSystem", "Failed to load asset {}", alr.Handle); + } + return asset; + } + + void RuntimeAssetSystem::QueueAssetLoad(const RuntimeAssetLoadRequest& request) + { + { + std::scoped_lock lock(m_AssetLoadingQueueMutex); + m_AssetLoadingQueue.push(request); + } + m_AssetLoadingQueueCV.notify_one(); + } + + Ref RuntimeAssetSystem::GetAsset(const AssetHandle sceneHandle, const AssetHandle assetHandle) + { + { + std::scoped_lock lock(m_AMLoadedAssetsMutex); + if (auto it = m_AMLoadedAssets.find(assetHandle); it != m_AMLoadedAssets.end()) + return it->second; + } + return TryLoadData({ sceneHandle, assetHandle }); + } + + bool RuntimeAssetSystem::RetrieveReadyAssets(std::vector>& outAssetList) + { + if (m_LoadedAssets.empty()) + return false; + + std::scoped_lock lock(m_LoadedAssetsMutex); + outAssetList = m_LoadedAssets; + m_LoadedAssets.clear(); + return true; + } + + void RuntimeAssetSystem::UpdateLoadedAssetList(const std::unordered_map>& loadedAssets) + { + std::scoped_lock lock(m_AMLoadedAssetsMutex); + m_AMLoadedAssets = loadedAssets; + } + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetSystem/RuntimeAssetSystem.h b/StarEngine/src/StarEngine/Asset/AssetSystem/RuntimeAssetSystem.h new file mode 100644 index 00000000..6afd8519 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetSystem/RuntimeAssetSystem.h @@ -0,0 +1,62 @@ +#pragma once + +#include "StarEngine/Asset/AssetMetadata.h" +#include "StarEngine/Core/Thread.h" +#include "StarEngine/Serialization/AssetPack.h" + +#include +#include +#include + +namespace StarEngine { + + class RuntimeAssetSystem : public RefCounted + { + public: + RuntimeAssetSystem(); + virtual ~RuntimeAssetSystem(); + + // Queue an asset for later loading (on asset thread) + void QueueAssetLoad(const RuntimeAssetLoadRequest& request); + + // Get an asset immediately (on asset thread). + // If the asset needs to be loaded, it will be loaded into "ready assets" and transfered back to main thread + // at next asset sync. + Ref GetAsset(const AssetHandle sceneHandle, const AssetHandle assetHandle); + + // Retrieve assets that have been loaded (from and earlier request) + bool RetrieveReadyAssets(std::vector>& outAssetList); + + // Replace the currently loaded asset collection with the given loadedAssets. + // This is effectively takes a "thread local" snapshot of the asset manager's loaded assets. + void UpdateLoadedAssetList(const std::unordered_map>& loadedAssets); + + void SetAssetPack(Ref assetPack) { m_AssetPack = assetPack; } + + void Stop(); + void StopAndWait(); + + private: + // The asset thread's mainline + void AssetThreadFunc(); + + Ref TryLoadData(const RuntimeAssetLoadRequest& request); + + private: + Thread m_Thread; + std::atomic m_Running = true; // not false. This ensures that if Stop() is called after the thread is dispatched but before it actually starts running, then the thread is correctly stopped. + + Ref m_AssetPack; + + std::queue m_AssetLoadingQueue; + std::mutex m_AssetLoadingQueueMutex; + std::condition_variable m_AssetLoadingQueueCV; + + std::vector> m_LoadedAssets; // Assets that have been loaded asynchronously and are waiting for sync back to Asset Manager + std::mutex m_LoadedAssetsMutex; + + std::unordered_map> m_AMLoadedAssets; // All currently loaded assets (synced from Asset Manager) + std::mutex m_AMLoadedAssetsMutex; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/AssetTypes.h b/StarEngine/src/StarEngine/Asset/AssetTypes.h new file mode 100644 index 00000000..82ef7668 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/AssetTypes.h @@ -0,0 +1,97 @@ +#pragma once + +#include "StarEngine/Core/Assert.h" + +namespace StarEngine { + + enum class AssetFlag : uint16_t + { + None = 0, + Missing = BIT(0), + Invalid = BIT(1) + }; + + enum class AssetType : uint16_t + { + None = 0, + Scene, + Prefab, + Mesh, + StaticMesh, + MeshSource, + Material, + Texture, + EnvMap, + Audio, + SoundConfig, + SpatializationConfig, + Font, + Script, + ScriptFile, + MeshCollider, + SoundGraphSound, + Skeleton, + Animation, + AnimationGraph + }; + + namespace Utils { + + inline AssetType AssetTypeFromString(std::string_view assetType) + { + if (assetType == "None") return AssetType::None; + if (assetType == "Scene") return AssetType::Scene; + if (assetType == "Prefab") return AssetType::Prefab; + if (assetType == "Mesh") return AssetType::Mesh; + if (assetType == "StaticMesh") return AssetType::StaticMesh; + if (assetType == "MeshAsset") return AssetType::MeshSource; // DEPRECATED + if (assetType == "MeshSource") return AssetType::MeshSource; + if (assetType == "Material") return AssetType::Material; + if (assetType == "Texture") return AssetType::Texture; + if (assetType == "EnvMap") return AssetType::EnvMap; + if (assetType == "Audio") return AssetType::Audio; + if (assetType == "SoundConfig") return AssetType::SoundConfig; + if (assetType == "Font") return AssetType::Font; + if (assetType == "Script") return AssetType::Script; + if (assetType == "ScriptFile") return AssetType::ScriptFile; + if (assetType == "MeshCollider") return AssetType::MeshCollider; + if (assetType == "SoundGraphSound") return AssetType::SoundGraphSound; + if (assetType == "Skeleton") return AssetType::Skeleton; + if (assetType == "Animation") return AssetType::Animation; + //if (assetType == "AnimationController") return AssetType::AnimationController; // OBSOLETE. You need to re-import animated asset + if (assetType == "AnimationGraph") return AssetType::AnimationGraph; + + return AssetType::None; + } + + inline const char* AssetTypeToString(AssetType assetType) + { + switch (assetType) + { + case AssetType::None: return "None"; + case AssetType::Scene: return "Scene"; + case AssetType::Prefab: return "Prefab"; + case AssetType::Mesh: return "Mesh"; + case AssetType::StaticMesh: return "StaticMesh"; + case AssetType::MeshSource: return "MeshSource"; + case AssetType::Material: return "Material"; + case AssetType::Texture: return "Texture"; + case AssetType::EnvMap: return "EnvMap"; + case AssetType::Audio: return "Audio"; + case AssetType::SoundConfig: return "SoundConfig"; + case AssetType::Font: return "Font"; + case AssetType::Script: return "Script"; + case AssetType::ScriptFile: return "ScriptFile"; + case AssetType::MeshCollider: return "MeshCollider"; + case AssetType::SoundGraphSound: return "SoundGraphSound"; + case AssetType::Skeleton: return "Skeleton"; + case AssetType::Animation: return "Animation"; + case AssetType::AnimationGraph: return "AnimationGraph"; + } + + SE_CORE_ASSERT(false, "Unknown Asset Type"); + return "None"; + } + + } +} diff --git a/StarEngine/src/StarEngine/Asset/AudioImporter.cpp b/StarEngine/src/StarEngine/Asset/AudioImporter.cpp deleted file mode 100644 index 8f731dab..00000000 --- a/StarEngine/src/StarEngine/Asset/AudioImporter.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "sepch.h" -#include "AudioImporter.h" - -#include "StarEngine/Audio/AudioEngine.h" - -#include "StarEngine/Project/Project.h" -#include "StarEngine/Scene/SceneSerializer.h" - -namespace StarEngine { - - Ref AudioImporter::ImportAudio(AssetHandle handle, const AssetMetadata& metadata) - { - SE_PROFILE_FUNCTION(); - - return LoadAudio(Project::GetActiveAssetDirectory() / metadata.FilePath); - } - - Ref AudioImporter::LoadAudio(const std::filesystem::path& path) - { - SE_PROFILE_FUNCTION_COLOR("AudioImporter::LoadAudio", 0xD17F8A); - Ref audioSource = CreateRef(); - - auto* engine = static_cast(AudioEngine::GetEngine()); - - { - SE_PROFILE_SCOPE_COLOR("AudioImporter::LoadAudio Scope", 0x3C7F8A); - - const ma_result result = ma_sound_init_from_file(engine, path.string().c_str(), MA_SOUND_FLAG_NO_SPATIALIZATION, nullptr, nullptr, audioSource->GetSound().get()); - if (result != MA_SUCCESS) - SE_CORE_ERROR("Failed to initialize sound: {}", path.string()); - } - - return audioSource; - } -} diff --git a/StarEngine/src/StarEngine/Asset/AudioImporter.h b/StarEngine/src/StarEngine/Asset/AudioImporter.h deleted file mode 100644 index ad3f8a9b..00000000 --- a/StarEngine/src/StarEngine/Asset/AudioImporter.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "Asset.h" -#include "AssetMetadata.h" - -#include "StarEngine/Audio/AudioSource.h" - -namespace StarEngine { - - class AudioImporter - { - public: - // AssetMetadata filepath is relative to project asset directory - static Ref ImportAudio(AssetHandle handle, const AssetMetadata& metadata); - - // Load from filepath - static Ref LoadAudio(const std::filesystem::path& path); - }; - -} diff --git a/StarEngine/src/StarEngine/Asset/EditorAssetManager.cpp b/StarEngine/src/StarEngine/Asset/EditorAssetManager.cpp deleted file mode 100644 index 18254e38..00000000 --- a/StarEngine/src/StarEngine/Asset/EditorAssetManager.cpp +++ /dev/null @@ -1,214 +0,0 @@ -#include "sepch.h" -#include "AssetManager.h" - -#include "AssetImporter.h" - -#include -#include - -#include -#include - -namespace fmt { - template <> - struct formatter : formatter { - auto format(const std::filesystem::path& path, format_context& ctx) const { - return formatter::format(path.string(), ctx); - } - }; -} // namespace fmt - -namespace StarEngine { - - static std::map s_AssetExtensionMap = { - { ".starscene", AssetType::Scene }, - { ".hazel", AssetType::Scene }, - { ".png", AssetType::Texture2D }, - { ".jpg", AssetType::Texture2D }, - { ".jpeg", AssetType::Texture2D }, - { ".mp3", AssetType::Audio }, - { ".wav", AssetType::Audio }, - { ".ogg", AssetType::Audio },/* - { ".obj", AssetType::ObjModel }, - { ".cs", AssetType::ScriptFile },*/ - }; - - static AssetType GetAssetTypeFromFileExtension(const std::filesystem::path& extension) - { - if (s_AssetExtensionMap.find(extension) == s_AssetExtensionMap.end()) - { - SE_CORE_WARN("Could not find AssetType for {}", extension); - return AssetType::None; - } - - return s_AssetExtensionMap.at(extension); - } - - YAML::Emitter& operator<<(YAML::Emitter& out, const std::string_view& v) - { - out << std::string(v.data(), v.size()); - return out; - } - - bool EditorAssetManager::IsAssetHandleValid(AssetHandle handle) const - { - return handle != 0 && m_AssetRegistry.find(handle) != m_AssetRegistry.end(); - } - - bool EditorAssetManager::IsAssetLoaded(AssetHandle handle) const - { - return m_LoadedAssets.find(handle) != m_LoadedAssets.end(); - } - - AssetType EditorAssetManager::GetAssetType(AssetHandle handle) const - { - if (!IsAssetHandleValid(handle)) - return AssetType::None; - - return m_AssetRegistry.at(handle).Type; - } - - void EditorAssetManager::ImportAsset(const std::filesystem::path& filepath) - { - AssetHandle handle; // generate new handle - AssetMetadata metadata; - metadata.FilePath = filepath; - metadata.Type = GetAssetTypeFromFileExtension(filepath.extension()); - SE_CORE_ASSERT(metadata.Type != AssetType::None); - Ref asset = AssetImporter::ImportAsset(handle, metadata); - if (asset) - { - asset->Handle = handle; - m_LoadedAssets[handle] = asset; - m_AssetRegistry[handle] = metadata; - SerializeAssetRegistry(); - } - } - - void EditorAssetManager::ImportScriptAsset(const std::filesystem::path& filepath, uint64_t uuid) - { - AssetHandle handle = uuid; // generate new handle - AssetMetadata metadata; - metadata.FilePath = filepath; - metadata.Type = GetAssetTypeFromFileExtension(filepath.extension()); - SE_CORE_ASSERT(metadata.Type != AssetType::None); - Ref asset = AssetImporter::ImportAsset(handle, metadata); - if (asset) - { - asset->Handle = handle; - m_LoadedAssets[handle] = asset; - m_AssetRegistry[handle] = metadata; - SerializeAssetRegistry(); - } - } - - const AssetMetadata& EditorAssetManager::GetMetadata(AssetHandle handle) const - { - SE_PROFILE_FUNCTION_COLOR("EditorAssetManager::GetMetadata", 0xF2A58A); - - static AssetMetadata s_NullMetadata; - auto it = m_AssetRegistry.find(handle); - if (it == m_AssetRegistry.end()) - return s_NullMetadata; - - return it->second; - } - - const std::filesystem::path& EditorAssetManager::GetFilePath(AssetHandle handle) const - { - return GetMetadata(handle).FilePath; - } - - Ref EditorAssetManager::GetAsset(AssetHandle handle) - { - SE_PROFILE_FUNCTION_COLOR("EditorAssetManager::GetAsset", 0xA3FFA4); - - // 1. check if handle is valid - if (!IsAssetHandleValid(handle)) - return nullptr; - - // 2. check if asset needs load (and if so, load) - Ref asset; - if (IsAssetLoaded(handle)) - { - SE_PROFILE_SCOPE_COLOR("EditorAssetManager::GetAsset Scope", 0xFF7200); - - asset = m_LoadedAssets.at(handle); - } - else - { - //SE_PROFILE_SCOPE_COLOR("EditorAssetManager::GetAsset 2 Scope", 0xA331F3); - - // load asset - const AssetMetadata& metadata = GetMetadata(handle); - asset = AssetImporter::ImportAsset(handle, metadata); - if (!asset) - { - // import failed - SE_CORE_ERROR("EditorAssetManager::GetAsset - asset import failed!"); - } - - m_LoadedAssets[handle] = asset; - } - - // 3. return asset - return asset; - } - - void EditorAssetManager::SerializeAssetRegistry() - { - auto path = Project::GetActiveAssetRegistryPath(); - - YAML::Emitter out; - { - out << YAML::BeginMap; // Root - out << YAML::Key << "AssetRegistry" << YAML::Value; - - out << YAML::BeginSeq; - for (const auto& [handle, metadata] : m_AssetRegistry) - { - out << YAML::BeginMap; - out << YAML::Key << "Handle" << YAML::Value << handle; - std::string filepathStr = metadata.FilePath.generic_string(); - out << YAML::Key << "FilePath" << YAML::Value << filepathStr; - out << YAML::Key << "Type" << YAML::Value << AssetTypeToString(metadata.Type); - out << YAML::EndMap; - } - out << YAML::EndSeq; - out << YAML::EndMap; // Root - } - - std::ofstream fout(path); - fout << out.c_str(); - - } - - bool EditorAssetManager::DeserializeAssetRegistry() - { - auto path = Project::GetActiveAssetRegistryPath(); - YAML::Node data; - try - { - data = YAML::LoadFile(path.string()); - } - catch (YAML::ParserException e) - { - SE_CORE_ERROR("Failed to load project file '{0}'\n {1}", path, e.what()); - return false; - } - - auto rootNode = data["AssetRegistry"]; - if (!rootNode) - return false; - - for (const auto& node : rootNode) - { - AssetHandle handle = node["Handle"].as(); - auto& metadata = m_AssetRegistry[handle]; - metadata.FilePath = node["FilePath"].as(); - metadata.Type = AssetTypeFromString(node["Type"].as()); - } - - return true; - } -} diff --git a/StarEngine/src/StarEngine/Asset/EditorAssetManager.h b/StarEngine/src/StarEngine/Asset/EditorAssetManager.h deleted file mode 100644 index 955c1cb6..00000000 --- a/StarEngine/src/StarEngine/Asset/EditorAssetManager.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include "AssetManagerBase.h" -#include "AssetMetadata.h" - -#include - -namespace StarEngine { - - using AssetRegistry = std::map; - - class EditorAssetManager : public AssetManagerBase - { - public: - virtual Ref GetAsset(AssetHandle handle) override; - - virtual bool IsAssetHandleValid(AssetHandle handle) const override; - virtual bool IsAssetLoaded(AssetHandle handle) const override; - virtual AssetType GetAssetType(AssetHandle handle) const override; - - void ImportAsset(const std::filesystem::path& filepath); - void ImportScriptAsset(const std::filesystem::path& filepath, uint64_t uuid); - - const AssetMetadata& GetMetadata(AssetHandle handle) const; - const std::filesystem::path& GetFilePath(AssetHandle handle) const; - - const AssetRegistry& GetAssetRegistry() const { return m_AssetRegistry; } - - void SerializeAssetRegistry(); - bool DeserializeAssetRegistry(); - - private: - AssetRegistry m_AssetRegistry; - AssetMap m_LoadedAssets; - - // TODO: memory-only assets - }; -} diff --git a/StarEngine/src/StarEngine/Asset/MeshColliderAsset.h b/StarEngine/src/StarEngine/Asset/MeshColliderAsset.h new file mode 100644 index 00000000..79d438fb --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/MeshColliderAsset.h @@ -0,0 +1,44 @@ +#pragma once + +#include "Asset.h" + +#include "StarEngine/Physics/ColliderMaterial.h" + +namespace StarEngine { + + enum class ECollisionComplexity : uint8_t + { + Default = 0, // Use simple for collision and complex for scene queries + UseComplexAsSimple = 1, // Use complex for collision AND scene queries + UseSimpleAsComplex = 2 // Use simple for collision AND scene queries + }; + + class MeshColliderAsset : public Asset + { + public: + AssetHandle ColliderMesh = 0; + ColliderMaterial Material; + bool EnableVertexWelding = true; + float VertexWeldTolerance = 0.1f; + bool FlipNormals = false; + bool CheckZeroAreaTriangles = true; + float AreaTestEpsilon = 0.06f; + bool ShiftVerticesToOrigin = false; + bool AlwaysShareShape = false; + ECollisionComplexity CollisionComplexity = ECollisionComplexity::Default; + glm::vec3 ColliderScale = glm::vec3(1.0f); + + // Preview Settings (Only used in mesh collider editor) + glm::vec3 PreviewScale = glm::vec3(1.0f); + + MeshColliderAsset() = default; + MeshColliderAsset(AssetHandle colliderMesh, ColliderMaterial material = ColliderMaterial()) + : ColliderMesh(colliderMesh), Material(material) + { + } + + static AssetType GetStaticType() { return AssetType::MeshCollider; } + virtual AssetType GetAssetType() const override { return GetStaticType(); } + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/MeshRuntimeSerializer.cpp b/StarEngine/src/StarEngine/Asset/MeshRuntimeSerializer.cpp new file mode 100644 index 00000000..93d9dfc5 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/MeshRuntimeSerializer.cpp @@ -0,0 +1,371 @@ +#include "sepch.h" +#include "MeshRuntimeSerializer.h" + +#include "MeshSourceFile.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Asset/AssimpMeshImporter.h" + +#include +#include + +namespace StarEngine { + + namespace Utils { + acl::iallocator& GetAnimationAllocator(); + }; + + struct MeshMaterial + { + std::string MaterialName; + std::string ShaderName; + + glm::vec3 AlbedoColor; + float Emission; + float Metalness; + float Roughness; + bool UseNormalMap; + + uint64_t AlbedoTexture; + uint64_t NormalTexture; + uint64_t MetalnessTexture; + uint64_t RoughnessTexture; + + static void Serialize(StreamWriter* serializer, const MeshMaterial& instance) + { + serializer->WriteString(instance.MaterialName); + serializer->WriteString(instance.ShaderName); + + serializer->WriteRaw(instance.AlbedoColor); + serializer->WriteRaw(instance.Emission); + serializer->WriteRaw(instance.Metalness); + serializer->WriteRaw(instance.Roughness); + serializer->WriteRaw(instance.UseNormalMap); + + serializer->WriteRaw(instance.AlbedoTexture); + serializer->WriteRaw(instance.NormalTexture); + serializer->WriteRaw(instance.MetalnessTexture); + serializer->WriteRaw(instance.RoughnessTexture); + } + + static void Deserialize(StreamReader* deserializer, MeshMaterial& instance) + { + deserializer->ReadString(instance.MaterialName); + deserializer->ReadString(instance.ShaderName); + + deserializer->ReadRaw(instance.AlbedoColor); + deserializer->ReadRaw(instance.Emission); + deserializer->ReadRaw(instance.Metalness); + deserializer->ReadRaw(instance.Roughness); + deserializer->ReadRaw(instance.UseNormalMap); + + deserializer->ReadRaw(instance.AlbedoTexture); + deserializer->ReadRaw(instance.NormalTexture); + deserializer->ReadRaw(instance.MetalnessTexture); + deserializer->ReadRaw(instance.RoughnessTexture); + } + }; + /* + static void Serialize(StreamWriter* serializer, const Skeleton& skeleton) + { + serializer->WriteArray(skeleton.GetBoneNames()); + serializer->WriteArray(skeleton.GetParentBoneIndices()); + serializer->WriteArray(skeleton.GetBoneTranslations()); + serializer->WriteArray(skeleton.GetBoneRotations()); + serializer->WriteArray(skeleton.GetBoneScales()); + } + + static void Deserialize(StreamReader* deserializer, Skeleton& skeleton) + { + std::vector boneNames; + std::vector parentBoneIndices; + std::vector boneTranslations; + std::vector boneRotations; + std::vector boneScales; + deserializer->ReadArray(boneNames); + deserializer->ReadArray(parentBoneIndices); + deserializer->ReadArray(boneTranslations); + deserializer->ReadArray(boneRotations); + deserializer->ReadArray(boneScales); + + skeleton.SetBones(std::move(boneNames), std::move(parentBoneIndices), std::move(boneTranslations), std::move(boneRotations), std::move(boneScales)); + } + + static void Serialize(StreamWriter* serializer, const Animation& animation) + { + serializer->WriteRaw(animation.GetDuration()); + serializer->WriteRaw(animation.GetNumTracks()); + + auto compressedTracks = static_cast(animation.GetData()); + serializer->WriteRaw(compressedTracks->get_size()); + serializer->WriteData(static_cast(animation.GetData()), compressedTracks->get_size()); + } + + static void Deserialize(StreamReader* deserializer, Animation& animation) + { + uint32_t numTracks; + float duration; + uint32_t compressedTracksSize; + + deserializer->ReadRaw(duration); + deserializer->ReadRaw(numTracks); + + deserializer->ReadRaw(compressedTracksSize); + void* buffer = Utils::GetAnimationAllocator().allocate(compressedTracksSize); + deserializer->ReadData(static_cast(buffer), compressedTracksSize); + + acl::error_result result; + acl::compressed_tracks* compressedTracks = acl::make_compressed_tracks(buffer, &result); + + if (!compressedTracks) + { + HZ_CORE_ERROR("Failed to deserialize animation: {0}", result.c_str()); + Utils::GetAnimationAllocator().deallocate(buffer, compressedTracksSize); + return; + } + + animation = std::move(Animation(duration, numTracks, compressedTracks)); + }*/ + + bool MeshRuntimeSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) + { + outInfo.Offset = stream.GetStreamPosition(); + + uint64_t streamOffset = stream.GetStreamPosition(); + + Ref meshSource = AssetManager::GetAsset(handle); + + MeshSourceFile file; + + bool hasMaterials = !meshSource->GetMaterials().empty(); + + // The meshSource might contain some animations. However, unless they are actually used in a scene they will not have been loaded (animation pointer is null) + // In this case we are not interested in serializing them for runtime. + size_t animationCount = std::count_if(std::begin(meshSource->m_Animations), std::end(meshSource->m_Animations), [](const auto& animation) { return animation.second != nullptr; }); + bool hasAnimation = animationCount != 0; + + bool hasSkeleton = meshSource->HasSkeleton(); + + file.Data.Flags = 0; + if (hasMaterials) + file.Data.Flags |= (uint32_t)MeshSourceFile::MeshFlags::HasMaterials; + if (hasAnimation) + file.Data.Flags |= (uint32_t)MeshSourceFile::MeshFlags::HasAnimation; + if (hasSkeleton) + file.Data.Flags |= (uint32_t)MeshSourceFile::MeshFlags::HasSkeleton; + + // Write header + stream.WriteRaw(file.Header); + // Leave space for Metadata + uint64_t metadataAbsolutePosition = stream.GetStreamPosition(); + stream.WriteZero(sizeof(MeshSourceFile::Metadata)); + + // Write nodes + file.Data.NodeArrayOffset = stream.GetStreamPosition() - streamOffset; + stream.WriteArray(meshSource->m_Nodes); + file.Data.NodeArraySize = (stream.GetStreamPosition() - streamOffset) - file.Data.NodeArrayOffset; + + // Write submeshes + file.Data.SubmeshArrayOffset = stream.GetStreamPosition() - streamOffset; + stream.WriteArray(meshSource->m_Submeshes); + file.Data.SubmeshArraySize = (stream.GetStreamPosition() - streamOffset) - file.Data.SubmeshArrayOffset; + + // Write Material Buffer + if (hasMaterials) + { + // Prepare materials + std::vector meshMaterials(meshSource->GetMaterials().size()); + const auto& meshSourceMaterials = meshSource->GetMaterials(); + for (size_t i = 0; i < meshMaterials.size(); i++) + { + MeshMaterial& material = meshMaterials[i]; + AssetHandle meshSourceMaterialHandle = meshSourceMaterials[i]; + Ref ma = AssetManager::GetAsset(meshSourceMaterialHandle); + Ref meshSourceMaterial = ma->GetMaterial(); + + material.MaterialName = meshSourceMaterial->GetName(); + material.ShaderName = meshSourceMaterial->GetShader()->GetName(); + + material.AlbedoColor = meshSourceMaterial->GetVector3("u_MaterialUniforms.AlbedoColor"); + material.Emission = meshSourceMaterial->GetFloat("u_MaterialUniforms.Emission"); + material.Metalness = meshSourceMaterial->GetFloat("u_MaterialUniforms.Metalness"); + material.Roughness = meshSourceMaterial->GetFloat("u_MaterialUniforms.Roughness"); + material.UseNormalMap = meshSourceMaterial->GetBool("u_MaterialUniforms.UseNormalMap"); + + auto albedoTexture = meshSourceMaterial->GetTexture2D("u_AlbedoTexture"); + auto normalTexture = meshSourceMaterial->GetTexture2D("u_NormalTexture"); + auto metalnessTexture = meshSourceMaterial->GetTexture2D("u_MetalnessTexture"); + auto roughnessTexture = meshSourceMaterial->GetTexture2D("u_RoughnessTexture"); + + material.AlbedoTexture = albedoTexture ? albedoTexture->Handle : AssetHandle(0); + material.NormalTexture = normalTexture ? normalTexture->Handle : AssetHandle(0); + material.MetalnessTexture = metalnessTexture ? metalnessTexture->Handle : AssetHandle(0); + material.RoughnessTexture = roughnessTexture ? roughnessTexture->Handle : AssetHandle(0); + } + + // Write materials + file.Data.MaterialArrayOffset = stream.GetStreamPosition() - streamOffset; + stream.WriteArray(meshMaterials); + file.Data.MaterialArraySize = (stream.GetStreamPosition() - streamOffset) - file.Data.MaterialArrayOffset; + } + else + { + // No materials + file.Data.MaterialArrayOffset = 0; + file.Data.MaterialArraySize = 0; + } + + // Write Vertex Buffer + file.Data.VertexBufferOffset = stream.GetStreamPosition() - streamOffset; + stream.WriteArray(meshSource->m_Vertices); + file.Data.VertexBufferSize = (stream.GetStreamPosition() - streamOffset) - file.Data.VertexBufferOffset; + + // Write Index Buffer + file.Data.IndexBufferOffset = stream.GetStreamPosition() - streamOffset; + stream.WriteArray(meshSource->m_Indices); + file.Data.IndexBufferSize = (stream.GetStreamPosition() - streamOffset) - file.Data.IndexBufferOffset; + /* + // Write Animation Data + if (hasAnimation || hasSkeleton) + { + file.Data.AnimationDataOffset = stream.GetStreamPosition() - streamOffset; + + if (hasSkeleton) + { + stream.WriteArray(meshSource->m_BoneInfluences); + stream.WriteArray(meshSource->m_BoneInfo); + Serialize(&stream, *meshSource->m_Skeleton); + } + + stream.WriteRaw((uint32_t)animationCount); + for (const auto&[hash, animation] : meshSource->m_Animations) + { + if (animation) + { + stream.WriteRaw(hash); + Serialize(&stream, *animation); + } + } + + file.Data.AnimationDataSize = (stream.GetStreamPosition() - streamOffset) - file.Data.AnimationDataOffset; + } + else + { + file.Data.AnimationDataOffset = 0; + file.Data.AnimationDataSize = 0; + }*/ + + file.Data.AnimationDataOffset = 0; + file.Data.AnimationDataSize = 0; + + // Write Metadata + uint64_t endOfStream = stream.GetStreamPosition(); + stream.SetStreamPosition(metadataAbsolutePosition); + stream.WriteRaw(file.Data); + stream.SetStreamPosition(endOfStream); + + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref MeshRuntimeSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) + { + stream.SetStreamPosition(assetInfo.PackedOffset); + uint64_t streamOffset = stream.GetStreamPosition(); + + MeshSourceFile file; + stream.ReadRaw(file.Header); + bool validHeader = memcmp(file.Header.HEADER, "HZMS", 4) == 0; + SE_CORE_ASSERT(validHeader); + if (!validHeader) + return nullptr; + + Ref meshSource = Ref::Create(); + meshSource->m_Runtime = true; + + stream.ReadRaw(file.Data); + + const auto& metadata = file.Data; + bool hasMaterials = metadata.Flags & (uint32_t)MeshSourceFile::MeshFlags::HasMaterials; + bool hasAnimation = metadata.Flags & (uint32_t)MeshSourceFile::MeshFlags::HasAnimation; + bool hasSkeleton = metadata.Flags & (uint32_t)MeshSourceFile::MeshFlags::HasSkeleton; + + stream.SetStreamPosition(metadata.NodeArrayOffset + streamOffset); + stream.ReadArray(meshSource->m_Nodes); + stream.SetStreamPosition(metadata.SubmeshArrayOffset + streamOffset); + stream.ReadArray(meshSource->m_Submeshes); + + if (hasMaterials) + { + std::vector meshMaterials; + stream.SetStreamPosition(metadata.MaterialArrayOffset + streamOffset); + stream.ReadArray(meshMaterials); + + meshSource->m_Materials.resize(meshMaterials.size()); + for (size_t i = 0; i < meshMaterials.size(); i++) + { + const auto& meshMaterial = meshMaterials[i]; + Ref material = Material::Create(Renderer::GetShaderLibrary()->Get(meshMaterial.ShaderName), meshMaterial.MaterialName); + auto ma = Ref::Create(material); + + ma->SetAlbedoColor(meshMaterial.AlbedoColor); + ma->SetEmission(meshMaterial.Emission); + ma->SetMetalness(meshMaterial.Metalness); + ma->SetRoughness(meshMaterial.Roughness); + ma->SetUseNormalMap(meshMaterial.UseNormalMap); + + // Get textures from AssetManager (note: this will potentially trigger additional loads) + // TODO(Yan): set maybe to runtime error texture if no asset is present + ma->SetAlbedoMap(meshMaterial.AlbedoTexture); + ma->SetNormalMap(meshMaterial.NormalTexture); + ma->SetMetalnessMap(meshMaterial.MetalnessTexture); + ma->SetRoughnessMap(meshMaterial.RoughnessTexture); + + AssetHandle maHandle = AssetManager::AddMemoryOnlyAsset(ma); + meshSource->m_Materials[i] = maHandle; + } + } + + stream.SetStreamPosition(metadata.VertexBufferOffset + streamOffset); + stream.ReadArray(meshSource->m_Vertices); + + stream.SetStreamPosition(metadata.IndexBufferOffset + streamOffset); + stream.ReadArray(meshSource->m_Indices); + /* + if (hasAnimation || hasSkeleton) + { + stream.SetStreamPosition(metadata.AnimationDataOffset + streamOffset); + if (hasSkeleton) + { + stream.ReadArray(meshSource->m_BoneInfluences); + stream.ReadArray(meshSource->m_BoneInfo); + + meshSource->m_Skeleton = CreateScope(); + Deserialize(&stream, *meshSource->m_Skeleton); + } + + uint32_t animationCount; + stream.ReadRaw(animationCount); + for (uint32_t i = 0; i < animationCount; i++) + { + size_t hash; + stream.ReadRaw(hash); + meshSource->m_Animations.emplace(hash, CreateScope()); + Deserialize(&stream, *meshSource->m_Animations[hash]); + } + }*/ + + if (!meshSource->m_Vertices.empty()) + meshSource->m_VertexBuffer = VertexBuffer::Create(Buffer(meshSource->m_Vertices.data(), (uint32_t)(meshSource->m_Vertices.size() * sizeof(Vertex)))); + + if (!meshSource->m_BoneInfluences.empty()) + meshSource->m_BoneInfluenceBuffer = VertexBuffer::Create(Buffer(meshSource->m_BoneInfluences.data(), (uint32_t)(meshSource->m_BoneInfluences.size() * sizeof(BoneInfluence)))); + + if(!meshSource->m_Indices.empty()) + meshSource->m_IndexBuffer = IndexBuffer::Create(Buffer(meshSource->m_Indices.data(), (uint32_t)(meshSource->m_Indices.size() * sizeof(Index)))); + + return meshSource; + } + +} diff --git a/StarEngine/src/StarEngine/Asset/MeshRuntimeSerializer.h b/StarEngine/src/StarEngine/Asset/MeshRuntimeSerializer.h new file mode 100644 index 00000000..03441c7c --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/MeshRuntimeSerializer.h @@ -0,0 +1,19 @@ +#pragma once + +#include "StarEngine/Asset/Asset.h" +#include "StarEngine/Asset/AssetSerializer.h" + +#include "StarEngine/Serialization/AssetPack.h" +#include "StarEngine/Serialization/AssetPackFile.h" +#include "StarEngine/Serialization/FileStream.h" + +namespace StarEngine { + + class MeshRuntimeSerializer + { + public: + bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo); + Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo); + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/MeshSerializer.cpp b/StarEngine/src/StarEngine/Asset/MeshSerializer.cpp new file mode 100644 index 00000000..82bbbf14 --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/MeshSerializer.cpp @@ -0,0 +1,343 @@ +#include "sepch.h" +#include "MeshSerializer.h" + +#include "yaml-cpp/yaml.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Project/Project.h" + +#include "AssimpMeshImporter.h" +#include "MeshRuntimeSerializer.h" + +#include "StarEngine/Debug/Profiler.h" + +namespace YAML { + + template<> + struct convert> + { + static Node encode(const std::vector& value) + { + Node node; + for (uint32_t element : value) + node.push_back(element); + return node; + } + + static bool decode(const Node& node, std::vector& result) + { + if (!node.IsSequence()) + return false; + + result.resize(node.size()); + for (size_t i = 0; i < node.size(); i++) + result[i] = node[i].as(); + + return true; + } + }; + +} + +namespace StarEngine { + + YAML::Emitter& operator<<(YAML::Emitter& out, const std::vector& value) + { + out << YAML::Flow; + out << YAML::BeginSeq; + for (uint32_t element : value) + out << element; + out << YAML::EndSeq; + return out; + } + + static std::string GetYAML(const AssetMetadata& metadata) + { + std::ifstream stream(Project::GetActiveAssetDirectory() / metadata.FilePath); + SE_CORE_ASSERT(stream); + std::stringstream strStream; + strStream << stream.rdbuf(); + return strStream.str(); + } + + ////////////////////////////////////////////////////////////////////////////////// + // MeshSourceSerializer + ////////////////////////////////////////////////////////////////////////////////// + + std::string SerializeToYAML(Ref mesh) + { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "Mesh"; + { + out << YAML::BeginMap; + out << YAML::Key << "MeshSource"; + out << YAML::Value << mesh->GetMeshSource(); + out << YAML::Key << "SubmeshIndices"; + out << YAML::Flow; + if (auto meshSource = AssetManager::GetAsset(mesh->GetMeshSource()); meshSource && meshSource->GetSubmeshes().size() == mesh->GetSubmeshes().size()) + out << YAML::Value << std::vector(); + else + out << YAML::Value << mesh->GetSubmeshes(); + out << YAML::EndMap; + } + out << YAML::EndMap; + + return std::string(out.c_str()); + } + + bool DeserializeFromYAML(const YAML::Node& data, Ref& targetMesh) + { + if (!data["Mesh"]) + return false; + + YAML::Node rootNode = data["Mesh"]; + if (!rootNode["MeshAsset"] && !rootNode["MeshSource"]) + return false; + + AssetHandle meshSource = 0; + if (rootNode["MeshAsset"]) // DEPRECATED + meshSource = rootNode["MeshAsset"].as(); + else + meshSource = rootNode["MeshSource"].as(); + + if (!AssetManager::GetAsset(meshSource)) + return false; // TODO(Yan): feedback to the user + + auto submeshIndices = rootNode["SubmeshIndices"].as>(); + auto generateColliders = rootNode["GenerateColliders"].as(false); + + targetMesh = Ref::Create(meshSource, submeshIndices, generateColliders); + return true; + } + + void RegisterMeshDependenciesFromYAML(const YAML::Node& data, AssetHandle handle) + { + Project::GetEditorAssetManager()->DeregisterDependencies(handle); + AssetHandle meshSourceHandle = 0; + if (auto rootNode = data["Mesh"]; rootNode) + { + meshSourceHandle = rootNode["MeshSource"].as(0); + } + + // must always register something, even if it's 0 + Project::GetEditorAssetManager()->RegisterDependency(meshSourceHandle, handle); + } + + bool MeshSourceSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + SE_PROFILE_FUNCTION("MeshSourceSerializer::TryLoadData"); + + AssimpMeshImporter importer(Project::GetEditorAssetManager()->GetFileSystemPathString(metadata)); + Ref meshSource = importer.ImportToMeshSource(); + if (!meshSource) + return false; + + asset = meshSource; + asset->Handle = metadata.Handle; + return true; + } + + bool MeshSourceSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + MeshRuntimeSerializer serializer; + serializer.SerializeToAssetPack(handle, stream, outInfo); + return true; + } + + Ref MeshSourceSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + MeshRuntimeSerializer serializer; + return serializer.DeserializeFromAssetPack(stream, assetInfo); + } + + ////////////////////////////////////////////////////////////////////////////////// + // MeshSerializer + ////////////////////////////////////////////////////////////////////////////////// + + void MeshSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref mesh = asset.As(); + + std::string yamlString = SerializeToYAML(mesh); + + std::filesystem::path serializePath = Project::GetActive()->GetAssetDirectory() / metadata.FilePath; + SE_CORE_WARN("Serializing to {0}", serializePath.string()); + std::ofstream fout(serializePath); + + if (!fout.is_open()) + { + SE_CORE_ERROR("Failed to serialize mesh to file '{0}'", serializePath); +// SE_CORE_ASSERT(false, "GetLastError() = {0}", GetLastError()); + return; + } + + fout << yamlString; + fout.flush(); + fout.close(); + } + + bool MeshSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + Ref mesh; + std::string yaml = GetYAML(metadata); + YAML::Node data = YAML::Load(yaml); + bool success = DeserializeFromYAML(data, mesh); + if (!success) + return false; + + mesh->Handle = metadata.Handle; + RegisterMeshDependenciesFromYAML(data, mesh->Handle); + asset = mesh; + return true; + } + + void MeshSerializer::RegisterDependencies(const AssetMetadata& metadata) const + { + YAML::Node data = YAML::Load(GetYAML(metadata)); + RegisterMeshDependenciesFromYAML(data, metadata.Handle); + } + + bool MeshSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + Ref mesh = AssetManager::GetAsset(handle); + + std::string yamlString = SerializeToYAML(mesh); + outInfo.Offset = stream.GetStreamPosition(); + stream.WriteString(yamlString); + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref MeshSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + std::string yamlString; + stream.ReadString(yamlString); + + Ref mesh; + YAML::Node data = YAML::Load(yamlString); + bool result = DeserializeFromYAML(data, mesh); + if (!result) + return nullptr; + + return mesh; + } + + ////////////////////////////////////////////////////////////////////////////////// + // StaticMeshSerializer + ////////////////////////////////////////////////////////////////////////////////// + + std::string SerializeToYAML(Ref staticMesh) + { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "Mesh"; + { + out << YAML::BeginMap; + out << YAML::Key << "MeshSource"; + out << YAML::Value << staticMesh->GetMeshSource(); + out << YAML::Key << "SubmeshIndices"; + out << YAML::Flow << YAML::Value << staticMesh->GetSubmeshes(); + out << YAML::EndMap; + } + out << YAML::EndMap; + + return std::string(out.c_str()); + } + + bool DeserializeFromYAML(const YAML::Node& data, Ref& targetStaticMesh) + { + if (!data["Mesh"]) + return false; + + YAML::Node rootNode = data["Mesh"]; + if (!rootNode["MeshAsset"] && !rootNode["MeshSource"]) + return false; + + AssetHandle meshSource = 0; + if (rootNode["MeshAsset"]) // DEPRECATED + meshSource = rootNode["MeshAsset"].as(); + else + meshSource = rootNode["MeshSource"].as(); + + auto submeshIndices = rootNode["SubmeshIndices"].as>(); + auto generateColliders = rootNode["GenerateColliders"].as(true); + + targetStaticMesh = Ref::Create(meshSource, submeshIndices, generateColliders); + return true; + } + + void RegisterStaticMeshDependenciesFromYAML(const YAML::Node& data, AssetHandle handle) + { + Project::GetEditorAssetManager()->DeregisterDependencies(handle); + AssetHandle meshSourceHandle = 0; + if (auto rootNode = data["Mesh"]; rootNode) + { + meshSourceHandle = rootNode["MeshSource"].as(0); + } + // must always register something, even if it's 0 + Project::GetEditorAssetManager()->RegisterDependency(meshSourceHandle, handle); + } + + void StaticMeshSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref staticMesh = asset.As(); + + std::string yamlString = SerializeToYAML(staticMesh); + + auto serializePath = Project::GetActive()->GetAssetDirectory() / metadata.FilePath; + std::ofstream fout(serializePath); + SE_CORE_ASSERT(fout.good()); + if (fout.good()) + fout << yamlString; + } + + bool StaticMeshSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + Ref staticMesh; + std::string yaml = GetYAML(metadata); + YAML::Node data = YAML::Load(yaml); + bool success = DeserializeFromYAML(data, staticMesh); + if (!success) + return false; + + staticMesh->Handle = metadata.Handle; + RegisterStaticMeshDependenciesFromYAML(data, staticMesh->Handle); + asset = staticMesh; + return true; + } + + void StaticMeshSerializer::RegisterDependencies(const AssetMetadata& metadata) const + { + YAML::Node data = YAML::Load(GetYAML(metadata)); + RegisterStaticMeshDependenciesFromYAML(data, metadata.Handle); + } + + bool StaticMeshSerializer::SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const + { + Ref staticMesh = AssetManager::GetAsset(handle); + + std::string yamlString = SerializeToYAML(staticMesh); + outInfo.Offset = stream.GetStreamPosition(); + stream.WriteString(yamlString); + outInfo.Size = stream.GetStreamPosition() - outInfo.Offset; + return true; + } + + Ref StaticMeshSerializer::DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const + { + stream.SetStreamPosition(assetInfo.PackedOffset); + std::string yamlString; + stream.ReadString(yamlString); + + Ref staticMesh; + YAML::Node data = YAML::Load(yamlString); + bool result = DeserializeFromYAML(data, staticMesh); + if (!result) + return nullptr; + + return staticMesh; + } + +} diff --git a/StarEngine/src/StarEngine/Asset/MeshSerializer.h b/StarEngine/src/StarEngine/Asset/MeshSerializer.h new file mode 100644 index 00000000..d44c213d --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/MeshSerializer.h @@ -0,0 +1,41 @@ +#pragma once + +#include "StarEngine/Asset/AssetSerializer.h" +#include "StarEngine/Serialization/FileStream.h" +#include "StarEngine/Renderer/Mesh.h" + +namespace StarEngine { + + class MeshSourceSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override {} + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + + class MeshSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + virtual void RegisterDependencies(const AssetMetadata& metadata) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + + class StaticMeshSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + virtual void RegisterDependencies(const AssetMetadata& metadata) const override; + + virtual bool SerializeToAssetPack(AssetHandle handle, FileStreamWriter& stream, AssetSerializationInfo& outInfo) const; + virtual Ref DeserializeFromAssetPack(FileStreamReader& stream, const AssetPackFile::AssetInfo& assetInfo) const; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/MeshSourceFile.h b/StarEngine/src/StarEngine/Asset/MeshSourceFile.h new file mode 100644 index 00000000..2ff0326c --- /dev/null +++ b/StarEngine/src/StarEngine/Asset/MeshSourceFile.h @@ -0,0 +1,54 @@ +#pragma once + +#include "StarEngine/Core/Base.h" +#include "StarEngine/Core/Math/AABB.h" + +#include + +namespace StarEngine { + + struct MeshSourceFile + { + enum class MeshFlags : uint32_t + { + HasMaterials = BIT(0), + HasAnimation = BIT(1), + HasSkeleton = BIT(2) + }; + + struct Metadata + { + uint32_t Flags; + AABB BoundingBox; + + uint64_t NodeArrayOffset; + uint64_t NodeArraySize; + + uint64_t SubmeshArrayOffset; + uint64_t SubmeshArraySize; + + uint64_t MaterialArrayOffset; + uint64_t MaterialArraySize; + + uint64_t VertexBufferOffset; + uint64_t VertexBufferSize; + + uint64_t IndexBufferOffset; + uint64_t IndexBufferSize; + + uint64_t AnimationDataOffset; + uint64_t AnimationDataSize; + }; + + struct FileHeader + { + const char HEADER[4] = { 'S','E','M','S' }; + uint32_t Version = 1; + // other metadata? + }; + + FileHeader Header; + Metadata Data; + }; + +} diff --git a/StarEngine/src/StarEngine/Asset/RuntimeAssetManager.h b/StarEngine/src/StarEngine/Asset/RuntimeAssetManager.h deleted file mode 100644 index e0046203..00000000 --- a/StarEngine/src/StarEngine/Asset/RuntimeAssetManager.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include "AssetManagerBase.h" - -namespace StarEngine { - - class RuntimeAssetManager : public AssetManagerBase - { - public: - }; -} diff --git a/StarEngine/src/StarEngine/Asset/SceneImporter.cpp b/StarEngine/src/StarEngine/Asset/SceneImporter.cpp deleted file mode 100644 index 6881609d..00000000 --- a/StarEngine/src/StarEngine/Asset/SceneImporter.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "sepch.h" -#include "SceneImporter.h" - -#include "StarEngine/Project/Project.h" -#include "StarEngine/Scene/SceneSerializer.h" -#include "StarEngine/Scripting/ScriptEngine.h" - -#include - -namespace StarEngine { - - Ref SceneImporter::ImportScene(AssetHandle handle, const AssetMetadata& metadata) - { - //SE_PROFILE_FUNCTION(); - - return LoadScene(Project::GetActiveAssetDirectory() / metadata.FilePath); - } - - Ref SceneImporter::LoadScene(const std::filesystem::path& path) - { - //SE_PROFILE_FUNCTION(); - - Ref scene = CreateRef(); - SceneSerializer serializer(scene); - serializer.Deserialize(path); - - return scene; - } - - void SceneImporter::SaveScene(Ref scene, const std::filesystem::path& path) - { - SceneSerializer serializer(scene); - serializer.Serialize(Project::GetActiveAssetDirectory() / path); - } -} diff --git a/StarEngine/src/StarEngine/Asset/SceneImporter.h b/StarEngine/src/StarEngine/Asset/SceneImporter.h deleted file mode 100644 index e3bcbb23..00000000 --- a/StarEngine/src/StarEngine/Asset/SceneImporter.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "Asset.h" -#include "AssetMetadata.h" - -#include "StarEngine/Scene/Scene.h" - -namespace StarEngine { - - class SceneImporter - { - public: - // AssetMetadata filepath is relative to project asset directory - static Ref ImportScene(AssetHandle handle, const AssetMetadata& metadata); - - // Load from filepath - static Ref LoadScene(const std::filesystem::path& path); - - static void SaveScene(Ref scene, const std::filesystem::path& path); - }; - -} diff --git a/StarEngine/src/StarEngine/Asset/TextureImporter.cpp b/StarEngine/src/StarEngine/Asset/TextureImporter.cpp index 9e32f3e9..0cea6927 100644 --- a/StarEngine/src/StarEngine/Asset/TextureImporter.cpp +++ b/StarEngine/src/StarEngine/Asset/TextureImporter.cpp @@ -1,57 +1,96 @@ #include "sepch.h" #include "TextureImporter.h" -#include "StarEngine/Project/Project.h" +#include "stb_image/stb_image.h" -#include +#include "StarEngine/Utilities/FileSystem.h" + +#include namespace StarEngine { - Ref TextureImporter::ImportTexture2D(AssetHandle handle, const AssetMetadata& metadata) + Buffer TextureImporter::ToBufferFromFile(const std::filesystem::path& path, ImageFormat& outFormat, uint32_t& outWidth, uint32_t& outHeight) { - //SE_PROFILE_FUNCTION(); - - return LoadTexture2D(Project::GetActiveAssetDirectory() / metadata.FilePath); - } + FileStatus fileStatus = FileSystem::TryOpenFileAndWait(path, 100); + Buffer imageBuffer; + std::string pathString = path.string(); + bool isSRGB = (outFormat == ImageFormat::SRGB) || (outFormat == ImageFormat::SRGBA); - Ref TextureImporter::LoadTexture2D(const std::filesystem::path& path) - { - //SE_PROFILE_FUNCTION(); int width, height, channels; - stbi_set_flip_vertically_on_load(1); - Buffer data; + void* tmp; + size_t size = 0; + if (stbi_is_hdr(pathString.c_str())) { - //SE_PROFILE_SCOPE("stbi_load - TextureImporter::ImportTexture2D"); - std::string pathStr = path.string(); - data.Data = stbi_load(pathStr.c_str(), &width, &height, &channels, 4); - channels = 4; + tmp = stbi_loadf(pathString.c_str(), &width, &height, &channels, 4); + if (tmp) + { + size = width * height * 4 * sizeof(float); + outFormat = ImageFormat::RGBA32F; + } + } + else + { + //stbi_set_flip_vertically_on_load(1); + tmp = stbi_load(pathString.c_str(), &width, &height, &channels, 4); + if (tmp) + { + size = width * height * 4; + outFormat = isSRGB ? ImageFormat::SRGBA : ImageFormat::RGBA; + } } - if (data.Data == nullptr) + if (!tmp) { - SE_CORE_ERROR("TextureImporter::ImportTexture2D - Could not load texture from filepath: {}", path.string()); - return nullptr; + return {}; } - // TODO: think about this - data.Size = width * height * channels; + SE_CORE_ASSERT(size > 0); + imageBuffer.Data = new byte[size]; // avoid `malloc+delete[]` mismatch. + imageBuffer.Size = size; + memcpy(imageBuffer.Data, tmp, size); + stbi_image_free(tmp); + + outWidth = width; + outHeight = height; + return imageBuffer; + } + + Buffer TextureImporter::ToBufferFromMemory(Buffer buffer, ImageFormat& outFormat, uint32_t& outWidth, uint32_t& outHeight) + { + Buffer imageBuffer; + + bool isSRGB = (outFormat == ImageFormat::SRGB) || (outFormat == ImageFormat::SRGBA); - TextureSpecification spec; - spec.Width = width; - spec.Height = height; - switch (channels) + int width, height, channels; + void* tmp; + size_t size; + + if (stbi_is_hdr_from_memory((const stbi_uc*)buffer.Data, (int)buffer.Size)) { - case 3: - spec.Format = ImageFormat::RGB8; - break; - case 4: - spec.Format = ImageFormat::RGBA8; - break; + tmp = (byte*)stbi_loadf_from_memory((const stbi_uc*)buffer.Data, (int)buffer.Size, &width, &height, &channels, STBI_rgb_alpha); + size = width * height * 4 * sizeof(float); + outFormat = ImageFormat::RGBA32F; } + else + { + // stbi_set_flip_vertically_on_load(1); + tmp = stbi_load_from_memory((const stbi_uc*)buffer.Data, (int)buffer.Size, &width, &height, &channels, STBI_rgb_alpha); + size = width * height * 4; + outFormat = isSRGB? ImageFormat::SRGBA : ImageFormat::RGBA; + } + + imageBuffer.Data = new byte[size]; // avoid `malloc+delete[]` mismatch. + imageBuffer.Size = size; + memcpy(imageBuffer.Data, tmp, size); + stbi_image_free(tmp); - Ref texture = Texture2D::Create(spec, data); - data.Release(); - return texture; + if (!imageBuffer.Data) + return {}; + + outWidth = width; + outHeight = height; + return imageBuffer; } + } diff --git a/StarEngine/src/StarEngine/Asset/TextureImporter.h b/StarEngine/src/StarEngine/Asset/TextureImporter.h index 82f5099c..2091d9ab 100644 --- a/StarEngine/src/StarEngine/Asset/TextureImporter.h +++ b/StarEngine/src/StarEngine/Asset/TextureImporter.h @@ -1,20 +1,17 @@ #pragma once -#include "Asset.h" -#include "AssetMetadata.h" - #include "StarEngine/Renderer/Texture.h" +#include + namespace StarEngine { class TextureImporter { public: - // AssetMetadata filepath is relative to project asset directory - static Ref ImportTexture2D(AssetHandle handle, const AssetMetadata& metadata); - - // Reads file directly from filesystem - // (i.e. path has to be relative / absolute to working directory) - static Ref LoadTexture2D(const std::filesystem::path& path); + static Buffer ToBufferFromFile(const std::filesystem::path& path, ImageFormat& outFormat, uint32_t& outWidth, uint32_t& outHeight); + static Buffer ToBufferFromMemory(Buffer buffer, ImageFormat& outFormat, uint32_t& outWidth, uint32_t& outHeight); + private: + const std::filesystem::path m_Path; }; } diff --git a/StarEngine/src/StarEngine/Audio/AudioEngine.cpp b/StarEngine/src/StarEngine/Audio/AudioEngine.cpp index a9f9fa88..46b7a90e 100644 --- a/StarEngine/src/StarEngine/Audio/AudioEngine.cpp +++ b/StarEngine/src/StarEngine/Audio/AudioEngine.cpp @@ -10,6 +10,7 @@ #undef STB_VORBIS_HEADER_ONLY #include "stb_vorbis.c" // Enables Vorbis decoding. +#include "StarEngine/Debug/Profiler.h" namespace StarEngine { @@ -17,7 +18,7 @@ namespace StarEngine { void AudioEngine::Init() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioEngine::Init"); s_ShuttingDown = false; ma_engine_config engineConfig = ma_engine_config_init(); @@ -36,7 +37,7 @@ namespace StarEngine { void AudioEngine::Shutdown() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioEngine::Shutdown"); s_ShuttingDown = true; ma_engine_stop(s_Engine); diff --git a/StarEngine/src/StarEngine/Audio/AudioEngine.h b/StarEngine/src/StarEngine/Audio/AudioEngine.h index c7d88de9..4e9fd5ae 100644 --- a/StarEngine/src/StarEngine/Audio/AudioEngine.h +++ b/StarEngine/src/StarEngine/Audio/AudioEngine.h @@ -1,5 +1,7 @@ #pragma once +#include + struct ma_engine; namespace StarEngine { diff --git a/StarEngine/src/StarEngine/Audio/AudioListener.cpp b/StarEngine/src/StarEngine/Audio/AudioListener.cpp index 9c0d41f9..7e87901b 100644 --- a/StarEngine/src/StarEngine/Audio/AudioListener.cpp +++ b/StarEngine/src/StarEngine/Audio/AudioListener.cpp @@ -4,12 +4,13 @@ #include #include "AudioEngine.h" +#include "StarEngine/Debug/Profiler.h" namespace StarEngine { void AudioListener::SetConfig(const AudioListenerConfig& config) const { - //NZ_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioListener::SetConfig"); auto* engine = static_cast(AudioEngine::GetEngine()); ma_engine_listener_set_cone(engine, m_ListenerIndex, config.ConeInnerAngle, config.ConeOuterAngle, config.ConeOuterGain); @@ -17,7 +18,7 @@ namespace StarEngine { void AudioListener::SetPosition(const glm::vec4& position) const { - //NZ_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioListener::SetPosition"); auto* engine = static_cast(AudioEngine::GetEngine()); ma_engine_listener_set_position(engine, m_ListenerIndex, position.x, position.y, position.z); @@ -32,7 +33,7 @@ namespace StarEngine { void AudioListener::SetDirection(const glm::vec3& forward) const { - //NZ_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioListener::SetDirection"); auto* engine = static_cast(AudioEngine::GetEngine()); ma_engine_listener_set_direction(engine, m_ListenerIndex, forward.x, forward.y, forward.z); @@ -40,7 +41,7 @@ namespace StarEngine { void AudioListener::SetVelocity(const glm::vec3& velocity) const { - //NZ_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioListener::SetVelocity"); auto* engine = static_cast(AudioEngine::GetEngine()); ma_engine_listener_set_velocity(engine, m_ListenerIndex, velocity.x, velocity.y, velocity.z); diff --git a/StarEngine/src/StarEngine/Audio/AudioListener.h b/StarEngine/src/StarEngine/Audio/AudioListener.h index a52228e7..75dc4efb 100644 --- a/StarEngine/src/StarEngine/Audio/AudioListener.h +++ b/StarEngine/src/StarEngine/Audio/AudioListener.h @@ -11,7 +11,7 @@ namespace StarEngine { float ConeOuterGain = 0.0f; }; - class AudioListener + class AudioListener : public RefCounted { public: AudioListener() = default; diff --git a/StarEngine/src/StarEngine/Audio/AudioSource.cpp b/StarEngine/src/StarEngine/Audio/AudioSource.cpp index 6e46ab23..cc595e94 100644 --- a/StarEngine/src/StarEngine/Audio/AudioSource.cpp +++ b/StarEngine/src/StarEngine/Audio/AudioSource.cpp @@ -2,21 +2,21 @@ #include "AudioSource.h" #include "AudioEngine.h" -#include "StarEngine/Asset/AudioImporter.h" +#include "StarEngine/Debug/Profiler.h" #include "StarEngine/Project/Project.h" namespace StarEngine { AudioSource::AudioSource() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::AudioSource"); m_Sound = std::make_unique(); } AudioSource::~AudioSource() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::~AudioSource"); if (!AudioEngine::ShuttingDownEngine()) { @@ -36,7 +36,7 @@ namespace StarEngine { void AudioSource::Play() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::Play"); if (m_Sound) { @@ -46,7 +46,7 @@ namespace StarEngine { void AudioSource::Pause() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::Pause"); if (m_Sound) { @@ -56,7 +56,7 @@ namespace StarEngine { void AudioSource::UnPause() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::UnPause"); if (m_Sound) { @@ -66,7 +66,7 @@ namespace StarEngine { void AudioSource::Stop() { - //NZ_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::Stop"); if (m_Sound) { @@ -79,7 +79,7 @@ namespace StarEngine { bool AudioSource::IsPlaying() { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::IsPlaying"); if (m_Sound.get()) return ma_sound_is_playing(m_Sound.get()); @@ -101,7 +101,7 @@ namespace StarEngine { static ma_attenuation_model GetAttenuationModel(AttenuationModelType model) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("ma_attenuation_model GetAttenuationModel"); switch (model) { @@ -116,7 +116,7 @@ namespace StarEngine { void AudioSource::SetConfig(AudioSourceConfig& config) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetConfig"); if (m_Sound) { @@ -159,7 +159,7 @@ namespace StarEngine { void AudioSource::SetVolume(float volume) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetVolume"); if (m_Sound) { @@ -169,7 +169,7 @@ namespace StarEngine { void AudioSource::SetPitch(float pitch) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetPitch"); if (m_Sound) { @@ -187,7 +187,7 @@ namespace StarEngine { void AudioSource::SetLooping(bool state) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetLooping"); if (m_Sound) { @@ -200,7 +200,7 @@ namespace StarEngine { void AudioSource::SetSpatialization(bool state) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetSpatialization"); m_Spatialization = state; if (m_Sound) @@ -211,7 +211,7 @@ namespace StarEngine { void AudioSource::SetAttenuationModel(AttenuationModelType type) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetAttenuationModel"); if (m_Sound) { @@ -224,7 +224,7 @@ namespace StarEngine { void AudioSource::SetRollOff(float rollOff) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetRollOff"); if (m_Sound) { @@ -234,7 +234,7 @@ namespace StarEngine { void AudioSource::SetMinGain(float minGain) { - //NZ_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetMinGain"); if (m_Sound) { @@ -244,7 +244,7 @@ namespace StarEngine { void AudioSource::SetMaxGain(float maxGain) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetMaxGain"); if (m_Sound) { @@ -254,7 +254,7 @@ namespace StarEngine { void AudioSource::SetMinDistance(float minDistance) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetMinDistance"); if (m_Sound) { @@ -264,7 +264,7 @@ namespace StarEngine { void AudioSource::SetMaxDistance(float maxDistance) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetMaxDistance"); if (m_Sound) { @@ -274,7 +274,7 @@ namespace StarEngine { void AudioSource::SetCone(float innerAngle, float outerAngle, float outerGain) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetCone"); if (m_Sound) { @@ -284,7 +284,7 @@ namespace StarEngine { void AudioSource::SetDopplerFactor(float factor) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetDopplerFactor"); if (m_Sound) { @@ -294,7 +294,7 @@ namespace StarEngine { void AudioSource::SetPosition(const glm::vec4& position) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetPosition"); if (m_Sound) { @@ -304,7 +304,7 @@ namespace StarEngine { void AudioSource::SetDirection(const glm::vec3& forward) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetDirection"); if (m_Sound) { @@ -314,7 +314,7 @@ namespace StarEngine { void AudioSource::SetVelocity(const glm::vec3& velocity) { - //SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("AudioSource::SetVelocity"); if (m_Sound) { diff --git a/StarEngine/src/StarEngine/Core/Application.cpp b/StarEngine/src/StarEngine/Core/Application.cpp index d3cb8695..da0e1859 100644 --- a/StarEngine/src/StarEngine/Core/Application.cpp +++ b/StarEngine/src/StarEngine/Core/Application.cpp @@ -1,157 +1,391 @@ #include "sepch.h" -#include "StarEngine/Core/Application.h" +#include "Application.h" -#include "StarEngine/Core/Log.h" - -#include "StarEngine/Core/Input.h" #include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Renderer/Framebuffer.h" +#include "StarEngine/Renderer/UI/Font.h" + +#include +#include +#include "StarEngine/ImGui/Colors.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Audio/AudioEngine.h" + +#include "Input.h" +#include "Memory.h" +#include "FatalSignal.h" + +#include "imgui_internal.h" + #include "StarEngine/Scripting/ScriptEngine.h" -#include "StarEngine/Utils/PlatformUtils.h" +#include "StarEngine/Utilities/StringUtils.h" +#include "StarEngine/Debug/Profiler.h" + +#include +#include + +#include "StarEngine/Editor/EditorApplicationSettings.h" +extern bool g_ApplicationRunning; +extern ImGuiContext* GImGui; namespace StarEngine { +#define BIND_EVENT_FN(fn) std::bind(&Application::##fn, this, std::placeholders::_1) + Application* Application::s_Instance = nullptr; + static std::thread::id s_MainThreadID; + Application::Application(const ApplicationSpecification& specification) - : m_Specification(specification) + : m_Specification(specification), m_RenderThread(specification.CoreThreadingPolicy), m_AppSettings("App.hsettings") { - SE_PROFILE_FUNCTION(); + //FatalSignal::Install(); - SE_CORE_ASSERT(!s_Instance, "Application already exists!"); s_Instance = this; - // Set working directory here - if (!m_Specification.WorkingDirectory.empty()) - std::filesystem::current_path(m_Specification.WorkingDirectory); + s_MainThreadID = std::this_thread::get_id(); + + m_AppSettings.Deserialize(); + + m_RenderThread.Run(); + + if (!specification.WorkingDirectory.empty()) + std::filesystem::current_path(specification.WorkingDirectory); + + m_Profiler = snew PerformanceProfiler(); + + Renderer::SetConfig(specification.RenderConfig); - m_Window = Window::Create(WindowProps(m_Specification.Name)); - m_Window->SetEventCallback(SE_BIND_EVENT_FN(Application::OnEvent)); + WindowSpecification windowSpec; + windowSpec.Title = specification.Name; + windowSpec.Width = specification.WindowWidth; + windowSpec.Height = specification.WindowHeight; + windowSpec.Decorated = specification.WindowDecorated; + windowSpec.Fullscreen = specification.Fullscreen; + windowSpec.VSync = specification.VSync; + windowSpec.IconPath = specification.IconPath; + m_Window = std::unique_ptr(Window::Create(windowSpec)); + m_Window->Init(); + m_Window->SetEventCallback([this](Event& e) { OnEvent(e); }); + // Load editor settings (will generate default settings if the file doesn't exist yet) + //EditorApplicationSettingsSerializer::Init(); + + SE_CORE_VERIFY(NFD::Init() == NFD_OKAY); + + // Init renderer and execute command queue to compile all shaders Renderer::Init(); + // Render one frame (TODO: maybe make a func called Pump or something) + m_RenderThread.Pump(); - m_ImGuiLayer = new ImGuiLayer(); - PushOverlay(m_ImGuiLayer); + if (specification.StartMaximized) + m_Window->Maximize(); + else + m_Window->CenterWindow(); + m_Window->SetResizable(specification.Resizable); + + if (m_Specification.EnableImGui) + { + m_ImGuiLayer = ImGuiLayer::Create(); + PushOverlay(m_ImGuiLayer); + } + + ScriptEngine::Init(); + AudioEngine::Init(); + Font::Init(); } Application::~Application() { - SE_PROFILE_FUNCTION(); - ScriptEngine::Shutdown(); + + NFD::Quit(); + + EditorApplicationSettingsSerializer::SaveSettings(); + + m_Window->SetEventCallback([](Event& e) {}); + + m_RenderThread.Terminate(); + + for (Layer* layer : m_LayerStack) + { + layer->OnDetach(); + delete layer; + } + + //ScriptEngine::Shutdown(); + Project::SetActive(nullptr); + Font::Shutdown(); + AudioEngine::Shutdown(); + Renderer::Shutdown(); + + delete m_Profiler; + m_Profiler = nullptr; } void Application::PushLayer(Layer* layer) { - SE_PROFILE_FUNCTION(); - m_LayerStack.PushLayer(layer); layer->OnAttach(); } void Application::PushOverlay(Layer* layer) { - SE_PROFILE_FUNCTION(); - m_LayerStack.PushOverlay(layer); layer->OnAttach(); } - void Application::Close() + void Application::PopLayer(Layer* layer) { - m_Running = false; + m_LayerStack.PopLayer(layer); + layer->OnDetach(); } - void Application::SubmitToMainThread(const std::function& function) + void Application::PopOverlay(Layer* layer) { - std::scoped_lock lock(m_MainThreadQueueMutex); - - m_MainThreadQueue.emplace_back(function); + m_LayerStack.PopOverlay(layer); + layer->OnDetach(); } - void Application::OnEvent(Event& e) + void Application::RenderImGui() { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("Application::RenderImGui"); + SE_SCOPE_PERF("Application::RenderImGui"); + + m_ImGuiLayer->Begin(); - EventDispatcher dispatcher(e); - dispatcher.Dispatch(SE_BIND_EVENT_FN(Application::OnWindowClose)); - dispatcher.Dispatch(SE_BIND_EVENT_FN(Application::OnWindowResize)); + for (int i = 0; i < m_LayerStack.Size(); i++) + m_LayerStack[i]->OnImGuiRender(); + } - for (auto it = m_LayerStack.rbegin(); it != m_LayerStack.rend(); ++it) + void Application::SyncEvents() + { + std::scoped_lock lock(m_EventQueueMutex); + for (auto& [synced, _] : m_EventQueue) { - if (e.Handled) - break; - (*it)->OnEvent(e); + synced = true; } } void Application::Run() { - SE_PROFILE_FUNCTION(); + SE_PROFILE_FUNCTION("Application::Run"); + OnInit(); while (m_Running) { SE_PROFILE_SCOPE("RunLoop"); - float time = Time::GetTime(); - Timestep timestep = time - m_LastFrameTime; - m_LastFrameTime = time; + //Wait for render thread to finish frame + { + SE_PROFILE_SCOPE("Wait"); + Timer timer; - ExecuteMainThreadQueue(); + m_RenderThread.BlockUntilRenderComplete(); + + m_PerformanceTimers.MainThreadWaitTime = timer.ElapsedMillis(); + } + + static uint64_t frameCounter = 0; + SE_CORE_INFO("-- BEGIN FRAME {0}", frameCounter); + + ProcessEvents(); + + m_ProfilerPreviousFrameData = m_Profiler->GetPerFrameData(); + m_Profiler->Clear(); + + m_RenderThread.NextFrame(); + + // Start rendering previous frame + m_RenderThread.Kick(); if (!m_Minimized) { - { - SE_PROFILE_SCOPE("LayerStack OnUpdate"); + Timer cpuTimer; + + // On Render Thread + Renderer::Submit([&]() + { + //m_Window->GetSwapChain().BeginFrame(); + m_Window->BeginFrame(); + }); + Renderer::BeginFrame(); + { + SE_SCOPE_PERF("Application Layer::OnUpdate"); for (Layer* layer : m_LayerStack) - layer->OnUpdate(timestep); + { + layer->OnUpdate(m_TimeStep); + } } - m_ImGuiLayer->Begin(); + Ref activeScene = ScriptEngine::GetInstance().CurrentScene(); + if (activeScene) { - SE_PROFILE_SCOPE("LayerStack OnImGuiRender"); + m_PerformanceTimers.ScriptUpdate = activeScene->GetPerformanceTimers().ScriptUpdate; + m_PerformanceTimers.PhysicsStepTime = activeScene->GetPerformanceTimers().PhysicsStep; + } - for (Layer* layer : m_LayerStack) - layer->OnImGuiRender(); + //Render ImGui on render Thread + Application* app = this; + if (m_Specification.EnableImGui) + { + Renderer::Submit([app]() { app->RenderImGui(); }); + Renderer::Submit([=]() { m_ImGuiLayer->End(); }); } - m_ImGuiLayer->End(); + Renderer::EndFrame(); + + //On Render thread + Renderer::Submit([&]() { + + //m_Window->GetSwapChain().BeginFrame(); + // Renderer::WaitAndRender(); + m_Window->Present(); + GetGraphicsDevice()->runGarbageCollection(); + }); + + m_CurrentFrameIndex = (m_CurrentFrameIndex + 1) % Renderer::GetConfig().FramesInFlight; + m_PerformanceTimers.MainThreadWorkTime = cpuTimer.ElapsedMillis(); } - m_Window->OnUpdate(); + //ScriptEngine::InitializeRuntimeDuplicatedEntities(); + Input::ClearReleasedKeys(); + + float time = GetTime(); + m_Frametime = time - m_LastFrameTime; + m_TimeStep = glm::min(m_Frametime, 0.0333f); + m_LastFrameTime = time; + + SE_CORE_INFO("-- END FRAME {0}", frameCounter); + frameCounter++; + + SE_PROFILE_MARK_FRAME; } + OnShutdown(); } - bool Application::OnWindowClose(WindowCloseEvent& e) + void Application::Close() { m_Running = false; - return true; } - bool Application::OnWindowResize(WindowResizeEvent& e) + void Application::OnShutdown() + { + m_EventCallbacks.clear(); + g_ApplicationRunning = false; + } + + void Application::ProcessEvents() + { + Input::TransitionPressedKeys(); + Input::TransitionPressedButtons(); + + m_Window->ProcessEvents(); + + // Note (0x): we have no control over what func() does. holding this lock while calling func() is a bad idea: + // 1) func() might be slow (means we hold the lock for ages) + // 2) func() might result in events getting queued, in which case we have a deadlock + std::scoped_lock lock(m_EventQueueMutex); + + // Process custom event queue, up until we encounter an event that is not yet sync'd + // If application queues such events, then it is the application's responsibility to call + // SyncEvents() at the appropriate time. + while (m_EventQueue.size() > 0) + { + const auto& [synced, func] = m_EventQueue.front(); + if (!synced) + { + break; + } + func(); + m_EventQueue.pop_front(); + } + } + + void Application::OnEvent(Event& event) { - SE_PROFILE_FUNCTION(); + EventDispatcher dispatcher(event); + dispatcher.Dispatch([this](WindowResizeEvent& e) { return OnWindowResize(e); }); + dispatcher.Dispatch([this](WindowMinimizeEvent& e) { return OnWindowMinimize(e); }); + dispatcher.Dispatch([this](WindowCloseEvent& e) { return OnWindowClose(e); }); + + for (auto it = m_LayerStack.end(); it != m_LayerStack.begin(); ) + { + (*--it)->OnEvent(event); + if (event.Handled) + break; + } - if (e.GetWidth() == 0 || e.GetHeight() == 0) + if (event.Handled) + return; + + // TODO(Peter): Should these callbacks be called BEFORE the layers recieve events? + // We may actually want that since most of these callbacks will be functions REQUIRED in order for the game + // to work, and if a layer has already handled the event we may end up with problems + for (auto& eventCallback : m_EventCallbacks) + { + eventCallback(event); + + if (event.Handled) + break; + } + + } + + bool Application::OnWindowResize(WindowResizeEvent& e) + { + const uint32_t width = e.GetWidth(), height = e.GetHeight(); + if (width == 0 || height == 0) { - m_Minimized = true; + //m_Minimized = true; return false; } + //m_Minimized = false; - m_Minimized = false; - Renderer::OnWindowResize(e.GetWidth(), e.GetHeight()); + auto& window = m_Window; + Renderer::Submit([&window, width, height]() mutable + { + //m_Window->GetDeviceManager()->ResizeSwapChain(); + //window->GetSwapChain().OnResize(width, height); + }); return false; } - void Application::ExecuteMainThreadQueue() + bool Application::OnWindowMinimize(WindowMinimizeEvent& e) { - std::scoped_lock lock(m_MainThreadQueueMutex); + m_Minimized = e.IsMinimized(); + return false; + } - for (auto& func : m_MainThreadQueue) - func(); + bool Application::OnWindowClose(WindowCloseEvent& e) + { + Close(); + return false; // give other things a chance to react to window close + } + + float Application::GetTime() const + { + return (float)glfwGetTime(); + } + + const char* Application::GetConfigurationName() + { + return SE_BUILD_CONFIG_NAME; + } - m_MainThreadQueue.clear(); + const char* Application::GetPlatformName() + { + return SE_BUILD_PLATFORM_NAME; + } + + std::thread::id Application::GetMainThreadID() { return s_MainThreadID; } + + bool Application::IsMainThread() + { + return std::this_thread::get_id() == s_MainThreadID; } } diff --git a/StarEngine/src/StarEngine/Core/Application.h b/StarEngine/src/StarEngine/Core/Application.h index f5516fc2..f115bb1d 100644 --- a/StarEngine/src/StarEngine/Core/Application.h +++ b/StarEngine/src/StarEngine/Core/Application.h @@ -1,86 +1,189 @@ #pragma once #include "StarEngine/Core/Base.h" - +#include "StarEngine/Core/TimeStep.h" +#include "StarEngine/Core/Timer.h" #include "StarEngine/Core/Window.h" #include "StarEngine/Core/LayerStack.h" -#include "StarEngine/Events/Event.h" -#include "StarEngine/Events/ApplicationEvent.h" +#include "StarEngine/Renderer/RendererConfig.h" -#include "StarEngine/Core/Timestep.h" +#include "StarEngine/Core/ApplicationSettings.h" +#include "StarEngine/Core/Events/ApplicationEvent.h" +#include "StarEngine/Core/RenderThread.h" #include "StarEngine/ImGui/ImGuiLayer.h" -int main(int argc, char** argv); +#include "nvrhi/nvrhi.h" + +#include namespace StarEngine { - struct ApplicationCommandLineArgs - { - int Count = 0; - char** Args = nullptr; - - const char* operator[](int index) const - { - SE_CORE_ASSERT(index < Count); - return Args[index]; - } - }; - struct ApplicationSpecification { - std::string Name = "StarEngine Application"; + std::string Name = "StarEngine"; + uint32_t WindowWidth = 1600, WindowHeight = 900; + bool WindowDecorated = false; + bool Fullscreen = false; + bool VSync = true; std::string WorkingDirectory; - ApplicationCommandLineArgs CommandLineArgs; + bool StartMaximized = true; + bool Resizable = true; + bool EnableImGui = true; + //ScriptEngineConfig ScriptConfig; + RendererConfig RenderConfig; + ThreadingPolicy CoreThreadingPolicy = ThreadingPolicy::MultiThreaded; + std::filesystem::path IconPath; }; class Application { - public: - Application(const ApplicationSpecification& specification); - virtual ~Application(); + using EventCallbackFn = std::function; + public: + struct PerformanceTimers + { + float MainThreadWorkTime = 0.0f; + float MainThreadWaitTime = 0.0f; + float RenderThreadWorkTime = 0.0f; + float RenderThreadWaitTime = 0.0f; + float RenderThreadGPUWaitTime = 0.0f; + + float ScriptUpdate = 0.0f; + float PhysicsStepTime = 0.0f; + }; + public: + Application(const ApplicationSpecification& specification); + virtual ~Application(); + + void Run(); + void Close(); + + virtual void OnInit() {} + virtual void OnShutdown(); + virtual void OnUpdate(Timestep ts) {} + + virtual void OnEvent(Event& event); + + void PushLayer(Layer* layer); + void PushOverlay(Layer* layer); + void PopLayer(Layer* layer); + void PopOverlay(Layer* layer); + void RenderImGui(); + + void AddEventCallback(const EventCallbackFn& eventCallback) { m_EventCallbacks.push_back(eventCallback); } + + void SetShowStats(bool show) { m_ShowStats = show; } + + template + void QueueEvent(Func&& func) + { + std::scoped_lock lock(m_EventQueueMutex); + m_EventQueue.emplace_back(true, func); + } + + // Creates & Dispatches an event either immediately, or adds it to an event queue which will be processed after the next call + // to SyncEvents(). + // Waiting until after next sync gives the application some control over _when_ the events will be processed. + // An example of where this is useful: + // Suppose an asset thread is loading assets and dispatching "AssetReloaded" events. + // We do not want those events to be processed until the asset thread has synced its assets back to the main thread. + template + void DispatchEvent(TEventArgs&&... args) + { + #ifndef SE_COMPILER_GCC + // TODO(Emily): GCC causes this to fail for AnimationGraphCompiledEvent for some reason. Investigate. + static_assert(std::is_assignable_v); + #endif + + std::shared_ptr event = std::make_shared(std::forward(args)...); + if constexpr (DispatchImmediately) + { + OnEvent(*event); + } + else + { + std::scoped_lock lock(m_EventQueueMutex); + m_EventQueue.emplace_back(false, [event]() { Application::Get().OnEvent(*event); }); + } + } + + // Mark all waiting events as sync'd. + // Thus allowing them to be processed on next call to ProcessEvents() + void SyncEvents(); + + inline Window& GetWindow() { return *m_Window; } + + static inline Application& Get() { return *s_Instance; } + + Timestep GetTimestep() const { return m_TimeStep; } + Timestep GetFrametime() const { return m_Frametime; } + float GetTime() const; // TODO: This should be in "Platform" + + static std::thread::id GetMainThreadID(); + static bool IsMainThread(); + + static const char* GetConfigurationName(); + static const char* GetPlatformName(); + + const ApplicationSpecification& GetSpecification() const { return m_Specification; } + + PerformanceProfiler* GetPerformanceProfiler() { return m_Profiler; } + + ImGuiLayer* GetImGuiLayer() { return m_ImGuiLayer; } + + RenderThread& GetRenderThread() { return m_RenderThread; } + uint32_t GetCurrentFrameIndex() const { return m_CurrentFrameIndex; } + const PerformanceTimers& GetPerformanceTimers() const { return m_PerformanceTimers; } + PerformanceTimers& GetPerformanceTimers() { return m_PerformanceTimers; } + const std::unordered_map& GetProfilerPreviousFrameData() const { return m_ProfilerPreviousFrameData; } - void OnEvent(Event& e); + ApplicationSettings& GetSettings() { return m_AppSettings; } + const ApplicationSettings& GetSettings() const { return m_AppSettings; } - void PushLayer(Layer* layer); - void PushOverlay(Layer* layer); + static bool IsRuntime() { return s_IsRuntime; } - Window& GetWindow() { return *m_Window; } + static DeviceManager* GetGraphicsDeviceManager() { return Application::Get().GetWindow().GetDeviceManager(); } + static nvrhi::DeviceHandle GetGraphicsDevice() { return GetGraphicsDeviceManager()->GetDevice(); } + private: + void ProcessEvents(); - void Close(); + bool OnWindowResize(WindowResizeEvent& e); + bool OnWindowMinimize(WindowMinimizeEvent& e); + bool OnWindowClose(WindowCloseEvent& e); + private: + std::unique_ptr m_Window; + ApplicationSpecification m_Specification; + bool m_Running = true, m_Minimized = false; + LayerStack m_LayerStack; + ImGuiLayer* m_ImGuiLayer; + Timestep m_Frametime; + Timestep m_TimeStep; + PerformanceProfiler* m_Profiler = nullptr; // TODO: Should be null in Dist + std::unordered_map m_ProfilerPreviousFrameData; + bool m_ShowStats = true; - ImGuiLayer* GetImGuiLayer() { return m_ImGuiLayer; } + RenderThread m_RenderThread; - static Application& Get() { return *s_Instance; } + std::mutex m_EventQueueMutex; + std::deque>> m_EventQueue; + std::vector m_EventCallbacks; - const ApplicationSpecification& GetSpecification() const { return m_Specification; } + float m_LastFrameTime = 0.0f; + uint32_t m_CurrentFrameIndex = 0; - void SubmitToMainThread(const std::function& function); - private: - void Run(); + PerformanceTimers m_PerformanceTimers; // TODO(Yan): remove for Dist - bool OnWindowClose(WindowCloseEvent& e); - bool OnWindowResize(WindowResizeEvent& e); + ApplicationSettings m_AppSettings; - void ExecuteMainThreadQueue(); - private: - ApplicationSpecification m_Specification; - Scope m_Window; - ImGuiLayer* m_ImGuiLayer; - bool m_Running = true; - bool m_Minimized = false; - LayerStack m_LayerStack; - float m_LastFrameTime = 0.0f; + static Application* s_Instance; - std::vector> m_MainThreadQueue; - std::mutex m_MainThreadQueueMutex; - private: - static Application* s_Instance; - friend int ::main(int argc, char** argv); + friend class Renderer; + protected: + inline static bool s_IsRuntime = false; }; - // To be defined in CLIENT - Application* CreateApplication(ApplicationCommandLineArgs args); + // Implemented by CLIENT + Application* CreateApplication(int argc, char** argv); } diff --git a/StarEngine/src/StarEngine/Core/ApplicationSettings.cpp b/StarEngine/src/StarEngine/Core/ApplicationSettings.cpp new file mode 100644 index 00000000..53cbd647 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/ApplicationSettings.cpp @@ -0,0 +1,118 @@ +#include "sepch.h" +#include "ApplicationSettings.h" + +#include "yaml-cpp/yaml.h" + +#include +#include + +namespace StarEngine { + + static void CreateDirectoriesIfNeeded(const std::filesystem::path& path) + { + std::filesystem::path directory = path.parent_path(); + if (!std::filesystem::exists(directory)) + std::filesystem::create_directories(directory); + } + + ApplicationSettings::ApplicationSettings(const std::filesystem::path& filepath) + : m_FilePath(filepath) + { + Deserialize(); + } + + void ApplicationSettings::Serialize() + { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "StarEngine Application Settings"; + out << YAML::Value; + + out << YAML::BeginMap; + for (const auto& [key, value] : m_Settings) + out << YAML::Key << key << YAML::Value << value; + + out << YAML::EndMap; + + out << YAML::EndSeq; + + CreateDirectoriesIfNeeded(m_FilePath); + std::ofstream fout(m_FilePath); + fout << out.c_str(); + + fout.close(); + } + + bool ApplicationSettings::Deserialize() + { + std::ifstream stream(m_FilePath); + if (!stream.good()) + return false; + + std::stringstream strStream; + strStream << stream.rdbuf(); + + YAML::Node data = YAML::Load(strStream.str()); + + auto settings = data["StarEngine Application Settings"]; + if (!settings) + return false; + + for (auto it = settings.begin(); it != settings.end(); it++) + { + const auto& key = it->first.as(); + const auto& value = it->second.as(); + m_Settings[key] = value; + } + + stream.close(); + return true; + } + + bool ApplicationSettings::HasKey(std::string_view key) const + { + return m_Settings.find(std::string(key)) != m_Settings.end(); + } + + std::string ApplicationSettings::Get(std::string_view name, const std::string& defaultValue) const + { + if (!HasKey(name)) + return defaultValue; + + return m_Settings.at(std::string(name)); + } + + float ApplicationSettings::GetFloat(std::string_view name, float defaultValue) const + { + if (!HasKey(name)) + return defaultValue; + + const std::string& string = m_Settings.at(std::string(name)); + return std::stof(string); + } + + int ApplicationSettings::GetInt(std::string_view name, int defaultValue) const + { + if (!HasKey(name)) + return defaultValue; + + const std::string& string = m_Settings.at(std::string(name)); + return std::stoi(string); + } + + void ApplicationSettings::Set(std::string_view name, std::string_view value) + { + m_Settings[std::string(name)] = value; + } + + void ApplicationSettings::SetFloat(std::string_view name, float value) + { + m_Settings[std::string(name)] = std::to_string(value); + } + + void ApplicationSettings::SetInt(std::string_view name, int value) + { + m_Settings[std::string(name)] = std::to_string(value); + } + +} diff --git a/StarEngine/src/StarEngine/Core/ApplicationSettings.h b/StarEngine/src/StarEngine/Core/ApplicationSettings.h new file mode 100644 index 00000000..c5fc9301 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/ApplicationSettings.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace StarEngine { + + class ApplicationSettings + { + public: + ApplicationSettings(const std::filesystem::path& filepath); + + void Serialize(); + bool Deserialize(); + + bool HasKey(std::string_view key) const; + + std::string Get(std::string_view name, const std::string& defaultValue = "") const; + float GetFloat(std::string_view name, float defaultValue = 0.0f) const; + int GetInt(std::string_view name, int defaultValue = 0) const; + + void Set(std::string_view name, std::string_view value); + void SetFloat(std::string_view name, float value); + void SetInt(std::string_view name, int value); + private: + std::filesystem::path m_FilePath; + std::map m_Settings; + }; + + +} diff --git a/StarEngine/src/StarEngine/Core/Assert.h b/StarEngine/src/StarEngine/Core/Assert.h index b6bceb62..cddd8ea6 100644 --- a/StarEngine/src/StarEngine/Core/Assert.h +++ b/StarEngine/src/StarEngine/Core/Assert.h @@ -1,43 +1,50 @@ #pragma once -#include "StarEngine/Core/Base.h" -#include "StarEngine/Core/Log.h" -#include +#include "Base.h" +#include "Log.h" -#ifdef SE_ENABLE_ASSERTS +#ifdef SE_PLATFORM_WINDOWS +#define SE_DEBUG_BREAK __debugbreak() +#elif defined(SE_COMPILER_CLANG) +#define SE_DEBUG_BREAK __builtin_debugtrap() +#else +#define SE_DEBUG_BREAK +#endif -// Alteratively we could use the same "default" message for both "WITH_MSG" and "NO_MSG" and -// provide support for custom formatting by concatenating the formatting string instead of having the format inside the default message -#define SE_INTERNAL_ASSERT_IMPL(type, check, msg, ...) { if(!(check)) { SE##type##ERROR(msg, __VA_ARGS__); SE_DEBUGBREAK(); } } -#define SE_INTERNAL_ASSERT_WITH_MSG(type, check, ...) SE_INTERNAL_ASSERT_IMPL(type, check, "Assertion failed: {0}", __VA_ARGS__) -#define SE_INTERNAL_ASSERT_NO_MSG(type, check) SE_INTERNAL_ASSERT_IMPL(type, check, "Assertion '{0}' failed at {1}:{2}", SE_STRINGIFY_MACRO(check), std::filesystem::path(__FILE__).filename().string(), __LINE__) +#ifdef SE_DEBUG +#define SE_ENABLE_ASSERTS +#endif -#define SE_INTERNAL_ASSERT_GET_MACRO_NAME(arg1, arg2, macro, ...) macro -#define SE_INTERNAL_ASSERT_GET_MACRO(...) SE_EXPAND_MACRO( SE_INTERNAL_ASSERT_GET_MACRO_NAME(__VA_ARGS__, SE_INTERNAL_ASSERT_WITH_MSG, SE_INTERNAL_ASSERT_NO_MSG) ) +#define SE_ENABLE_VERIFY -// Currently accepts at least the condition and one additional parameter (the message) being optional -#define SE_ASSERT(...) SE_EXPAND_MACRO( SE_INTERNAL_ASSERT_GET_MACRO(__VA_ARGS__)(_, __VA_ARGS__) ) -#define SE_CORE_ASSERT(...) SE_EXPAND_MACRO( SE_INTERNAL_ASSERT_GET_MACRO(__VA_ARGS__)(_CORE_, __VA_ARGS__) ) +#ifdef SE_ENABLE_ASSERTS +#ifdef SE_COMPILER_CLANG +#define SE_CORE_ASSERT_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Core, "Assertion Failed", ##__VA_ARGS__) +#define SE_ASSERT_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Client, "Assertion Failed", ##__VA_ARGS__) #else -#define SE_ASSERT(...) -#define SE_CORE_ASSERT(...) +#define SE_CORE_ASSERT_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Core, "Assertion Failed" __VA_OPT__(,) __VA_ARGS__) +#define SE_ASSERT_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Client, "Assertion Failed" __VA_OPT__(,) __VA_ARGS__) #endif -#ifdef SE_ENABLE_VERIFY - -// Alteratively we could use the same "default" message for both "WITH_MSG" and "NO_MSG" and -// provide support for custom formatting by concatenating the formatting string instead of having the format inside the default message -#define SE_INTERNAL_VERIFY_IMPL(type, check, msg, ...) { if(!(check)) { SE##type##ERROR(msg, __VA_ARGS__); SE_DEBUGBREAK(); } } -#define SE_INTERNAL_VERIFY_WITH_MSG(type, check, ...) SE_INTERNAL_VERIFY_IMPL(type, check, "Assertion failed: {0}", __VA_ARGS__) -#define SE_INTERNAL_VERIFY_NO_MSG(type, check) SE_INTERNAL_VERIFY_IMPL(type, check, "Assertion '{0}' failed at {1}:{2}", SE_STRINGIFY_MACRO(check), std::filesystem::path(__FILE__).filename().string(), __LINE__) +#define SE_CORE_ASSERT(condition, ...) { if(!(condition)) { SE_CORE_ASSERT_MESSAGE_INTERNAL(__VA_ARGS__); SE_DEBUG_BREAK; } } +#define SE_ASSERT(condition, ...) { if(!(condition)) { SE_ASSERT_MESSAGE_INTERNAL(__VA_ARGS__); SE_DEBUG_BREAK; } } +#else +#define SE_CORE_ASSERT(condition, ...) +#define SE_ASSERT(condition, ...) +#endif -#define SE_INTERNAL_VERIFY_GET_MACRO_NAME(arg1, arg2, macro, ...) macro -#define SE_INTERNAL_VERIFY_GET_MACRO(...) SE_EXPAND_MACRO( SE_INTERNAL_VERIFY_GET_MACRO_NAME(__VA_ARGS__, SE_INTERNAL_VERIFY_WITH_MSG, SE_INTERNAL_VERIFY_NO_MSG) ) +#ifdef SE_ENABLE_VERIFY +#ifdef SE_COMPILER_CLANG +#define SE_CORE_VERIFY_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Core, "Verify Failed", ##__VA_ARGS__) +#define SE_VERIFY_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Client, "Verify Failed", ##__VA_ARGS__) +#else +#define SE_CORE_VERIFY_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Core, "Verify Failed" __VA_OPT__(,) __VA_ARGS__) +#define SE_VERIFY_MESSAGE_INTERNAL(...) ::StarEngine::Log::PrintAssertMessage(::StarEngine::Log::Type::Client, "Verify Failed" __VA_OPT__(,) __VA_ARGS__) +#endif -// Currently accepts at least the condition and one additional parameter (the message) being optional -#define SE_VERIFY(...) SE_EXPAND_MACRO( SE_INTERNAL_VERIFY_GET_MACRO(__VA_ARGS__)(_, __VA_ARGS__) ) -#define SE_CORE_VERIFY(...) SE_EXPAND_MACRO( SE_INTERNAL_VERIFY_GET_MACRO(__VA_ARGS__)(_CORE_, __VA_ARGS__) ) +#define SE_CORE_VERIFY(condition, ...) { if(!(condition)) { SE_CORE_VERIFY_MESSAGE_INTERNAL(__VA_ARGS__); SE_DEBUG_BREAK; } } +#define SE_VERIFY(condition, ...) { if(!(condition)) { SE_VERIFY_MESSAGE_INTERNAL(__VA_ARGS__); SE_DEBUG_BREAK; } } #else -#define SE_VERIFY(...) -#define SE_CORE_VERIFY(...) +#define SE_CORE_VERIFY(condition, ...) +#define SE_VERIFY(condition, ...) #endif diff --git a/StarEngine/src/StarEngine/Core/Base.cpp b/StarEngine/src/StarEngine/Core/Base.cpp new file mode 100644 index 00000000..abd50abc --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Base.cpp @@ -0,0 +1,25 @@ +#include "sepch.h" +#include "Base.h" + +#include "Log.h" +#include "Memory.h" + +namespace StarEngine { + + void InitializeCore() + { + Allocator::Init(); + Log::Init(); + + SE_CORE_TRACE_TAG("Core", "StarEngine {}", SE_VERSION); + SE_CORE_TRACE_TAG("Core", "Intializing..."); + } + + void ShutdownCore() + { + SE_CORE_TRACE_TAG("Core", "Shutting down..."); + + Log::Shutdown(); + } + +} diff --git a/StarEngine/src/StarEngine/Core/Base.h b/StarEngine/src/StarEngine/Core/Base.h index f47f2478..b91b787e 100644 --- a/StarEngine/src/StarEngine/Core/Base.h +++ b/StarEngine/src/StarEngine/Core/Base.h @@ -1,35 +1,69 @@ #pragma once -#include "StarEngine/Core/PlatformDetection.h" - +#include "StarEngine/Core/Ref.h" +#include #include -#if defined(SE_PLATFORM_WINDOWS) -#define SE_DEBUGBREAK() __debugbreak() -#elif defined(SE_PLATFORM_LINUX) -#include -#define SE_DEBUGBREAK() raise(SIGTRAP) +namespace StarEngine +{ + void InitializeCore(); + void ShutdownCore(); +}; + +#if defined(_WIN64) || defined(_WIN32) + #define SE_PLATFORM_WINDOWS +#elif defined(__linux__) + #define SE_PLATFORM_LINUX + #define SE_PLATFORM_UNIX +#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) + #define SE_PLATFORM_BSD + #define SE_PLATFORM_UNIX +#elif defined(__unix__) || defined(__unix) + #define SE_PLATFORM_UNIX #else -#error "Platform doesn't support debugbreak yet!" + #error "Unsupported platform! StarEngine supports Windows, Linux, and BSD." #endif -#ifdef SE_DEBUG -#define SE_ENABLE_ASSERTS -#endif +#define BIT(x) (1u << x) -#ifndef SE_DIST -#define SE_ENABLE_VERIFY -#endif +//------------------------------------------------------------------------------ +// Compiler Detection +//------------------------------------------------------------------------------ -#define SE_EXPAND_MACRO(x) x -#define SE_STRINGIFY_MACRO(x) #x +#if defined(__clang__) + #define SE_COMPILER_CLANG +#elif defined(__GNUC__) + #define SE_COMPILER_GCC +#elif defined(_MSC_VER) + #define SE_COMPILER_MSVC +#else + #error "Unknown compiler! StarEngine only supports MSVC, GCC, and Clang." +#endif -#define BIT(x) (1 << x) +//------------------------------------------------------------------------------ +// Function Inlining & Static Declaration +//------------------------------------------------------------------------------ -#define SE_BIND_EVENT_FN(fn) [this](auto&&... args) -> decltype(auto) { return this->fn(std::forward(args)...); } +#if defined(SE_COMPILER_MSVC) + #define SE_FORCE_INLINE __forceinline + #define SE_EXPLICIT_STATIC static +#elif defined(SE_COMPILER_GCC) || defined(SE_COMPILER_CLANG) + #define SE_FORCE_INLINE __attribute__((always_inline)) inline + #define SE_EXPLICIT_STATIC +#else + #define SE_FORCE_INLINE inline + #define SE_EXPLICIT_STATIC +#endif +// Pointer wrappers namespace StarEngine { + template + T RoundDown(T x, T fac) { return x / fac * fac; } + + template + T RoundUp(T x, T fac) { return RoundDown(x + fac - 1, fac); } + // Pointer wrappers template using Scope = std::unique_ptr; template @@ -38,15 +72,42 @@ namespace StarEngine { return std::make_unique(std::forward(args)...); } - template - using Ref = std::shared_ptr; - template - constexpr Ref CreateRef(Args&& ... args) + using byte = uint8_t; + + /** A simple wrapper for std::atomic_flag to avoid confusing + function names usage. The object owning it can still be + default copyable, but the copied flag is going to be reset. + */ + struct AtomicFlag { - return std::make_shared(std::forward(args)...); - } + SE_FORCE_INLINE void SetDirty() { flag.clear(); } + SE_FORCE_INLINE bool CheckAndResetIfDirty() { return !flag.test_and_set(); } -} + explicit AtomicFlag() noexcept { flag.test_and_set(); } + AtomicFlag(const AtomicFlag&) noexcept {} + AtomicFlag& operator=(const AtomicFlag&) noexcept { return *this; } + AtomicFlag(AtomicFlag&&) noexcept {}; + AtomicFlag& operator=(AtomicFlag&&) noexcept { return *this; } + + private: + std::atomic_flag flag; + }; -#include "StarEngine/Core/Log.h" -#include "StarEngine/Core/Assert.h" + struct Flag + { + SE_FORCE_INLINE void SetDirty() noexcept { flag = true; } + SE_FORCE_INLINE bool CheckAndResetIfDirty() noexcept + { + if (flag) + return !(flag = !flag); + else + return false; + } + + SE_FORCE_INLINE bool IsDirty() const noexcept { return flag; } + + private: + bool flag = false; + }; + +} diff --git a/StarEngine/src/StarEngine/Core/Buffer.h b/StarEngine/src/StarEngine/Core/Buffer.h index 9cccb7c5..0a1ca99d 100644 --- a/StarEngine/src/StarEngine/Core/Buffer.h +++ b/StarEngine/src/StarEngine/Core/Buffer.h @@ -1,94 +1,148 @@ #pragma once -#include -#include +#include "StarEngine//Core/Assert.h" + +#include namespace StarEngine { struct Buffer { - uint8_t* Data = nullptr; + void* Data = nullptr; uint64_t Size = 0; Buffer() = default; - Buffer(uint64_t size) - { - Allocate(size); + Buffer(const void* data, uint64_t size) + : Data((void*)data), Size(size) { } - Buffer(const void* data, uint64_t size) - : Data((uint8_t*)data), Size(size) + template + Buffer(const std::array& array) + : Data(array.data()), Size(array.size() * sizeof(T)) { + } + + template + Buffer(const std::vector& vector) + : Data(vector.data()), Size(vector.size() * sizeof(T)) { } - Buffer(const Buffer&) = default; + static Buffer Copy(const Buffer& other) + { + Buffer buffer; + buffer.Allocate(other.Size); + memcpy(buffer.Data, other.Data, other.Size); + return buffer; + } - static Buffer Copy(Buffer other) + static Buffer Copy(const void* data, uint64_t size) { - Buffer result(other.Size); - memcpy(result.Data, other.Data, other.Size); - return result; + Buffer buffer; + buffer.Allocate(size); + if (size) memcpy(buffer.Data, data, size); + return buffer; } void Allocate(uint64_t size) { - Release(); - - Data = (uint8_t*)malloc(size); + delete[](byte*)Data; + Data = nullptr; Size = size; + + if (size == 0) + return; + + Data = snew byte[size]; + } + + void Reallocate(uint64_t size) + { + Release(); + Allocate(size); } void Release() { - free(Data); + delete[](byte*)Data; Data = nullptr; Size = 0; } + void ZeroInitialize() + { + if (Data) + memset(Data, 0, Size); + } + template - T* As() + T& Read(uint64_t offset = 0) { - return (T*)Data; + return *(T*)((byte*)Data + offset); } - operator bool() const + template + const T& Read(uint64_t offset = 0) const { - return (bool)Data; + return *(T*)((byte*)Data + offset); } - }; + byte* ReadBytes(uint64_t size, uint64_t offset) const + { + SE_CORE_ASSERT(offset + size <= Size, "Buffer overflow!"); + byte* buffer = snew byte[size]; + memcpy(buffer, (byte*)Data + offset, size); + return buffer; + } - struct ScopedBuffer - { - ScopedBuffer(Buffer buffer) - : m_Buffer(buffer) + void Write(Buffer buffer, uint64_t offset = 0) { + SE_CORE_ASSERT(offset + buffer.Size <= Size, "Buffer overflow!"); + memcpy((byte*)Data + offset, buffer.Data, buffer.Size); } - ScopedBuffer(uint64_t size) - : m_Buffer(size) + void Write(const void* data, uint64_t size, uint64_t offset = 0) { + Write(Buffer(data, size), offset); } - ~ScopedBuffer() + operator bool() const { - m_Buffer.Release(); + return (bool)Data; } - uint8_t* Data() { return m_Buffer.Data; } - uint64_t Size() { return m_Buffer.Size; } + byte& operator[](int index) + { + return ((byte*)Data)[index]; + } + + byte operator[](int index) const + { + return ((byte*)Data)[index]; + } template - T* As() + T* As() const { - return m_Buffer.As(); + return (T*)Data; } - operator bool() const { return m_Buffer; } - private: - Buffer m_Buffer; + inline uint64_t GetSize() const { return Size; } }; + struct BufferSafe : public Buffer + { + ~BufferSafe() + { + Release(); + } + static BufferSafe Copy(const void* data, uint64_t size) + { + BufferSafe buffer; + buffer.Allocate(size); + memcpy(buffer.Data, data, size); + return buffer; + } + }; } diff --git a/StarEngine/src/StarEngine/Core/Delegate.h b/StarEngine/src/StarEngine/Core/Delegate.h new file mode 100644 index 00000000..4b0f48e1 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Delegate.h @@ -0,0 +1,292 @@ +/* + Based on: + - original work by Sergey Ryazanov (MIT) + "The Impossibly Fast C++ Delegates", 18 Jul 2005 + https://www.codeproject.com/articles/11015/the-impossibly-fast-c-delegates + + - and the work of Sergey Alexandrovich Kryukov (MIT) + "The Impossibly Fast C++ Delegates, Fixed", 11 Mar 2017 + https://www.codeproject.com/Articles/1170503/The-Impossibly-Fast-Cplusplus-Delegates-Fixed +*/ + +#pragma once + +#include + +namespace StarEngine { + + template + class Delegate; + + template + class MulticastDelegate; + + //========================================================================== + /** Simple functiondelegate to bind a callback of some sort without + unnecessary allocations. Faster than std::function. + */ + template + class Delegate + { + friend class MulticastDelegate; + + using TInstancePtr = void*; + using TInternalFunction = TReturn(*)(TInstancePtr, TArgs...); + + struct InvocationElement + { + InvocationElement() = default; + InvocationElement(TInstancePtr thisPtr, TInternalFunction aStub) : Object(thisPtr), Stub(aStub) {} + + bool operator ==(const InvocationElement& another) const { return another.Stub == Stub && another.Object == Object; } + bool operator !=(const InvocationElement& another) const { return another.Stub != Stub || another.Object != Object; } + + TInstancePtr Object = nullptr; + TInternalFunction Stub = nullptr; + }; + + public: + Delegate() = default; + Delegate(const Delegate& other) { m_Invocation = other.m_Invocation; } + Delegate& operator =(const Delegate& other) { m_Invocation = other.m_Invocation; return *this; } + bool operator == (const Delegate& other) const { return m_Invocation == other.m_Invocation; } + bool operator != (const Delegate& other) const { return m_Invocation != other.m_Invocation; } + + //========================================================================== + /// Free function binding + template + void Bind() + { + Assign(nullptr, FreeFunctionStub); + } + + /// Lambda binding. For now this only forks with persistent or captureless + /// lambdas. (TODO: store copy of the lambda) + template + void BindLambda(const TLambda& lambda) + { + Assign((TInstancePtr)(&lambda), LambdaStub); + } + + /// Member function binding + template + void Bind(TClass* object) + { + using TMembFunc = TReturn(TClass::*)(TArgs...); + using TMembFuncConst = TReturn(TClass::*)(TArgs...) const; + + if constexpr (std::is_same_v) + { + Assign(const_cast(object), ConstMemberFunctionStub); + } + else + { + static_assert(std::is_same_v, "Invalid function signature."); // TODO: C++20 'requires' woul've solved this + Assign((TInstancePtr)(object), MemberFunctionStub); + } + } + + //========================================================================== + /// Unbind any binding + void Unbind() + { + m_Invocation = InvocationElement(); + } + + //========================================================================== + bool IsBound() const { return m_Invocation.Stub; } + operator bool() const { return IsBound(); } + + TReturn Invoke(TArgs...args) const + { + SE_CORE_ASSERT(IsBound(), "Trying to invoke unbound delegate."); + return std::invoke(m_Invocation.Stub, m_Invocation.Object, std::forward(args)...); + } + + private: + inline void Assign(TInstancePtr anObject, TInternalFunction aStub) + { + m_Invocation.Object = anObject; + m_Invocation.Stub = aStub; + } + + template + static TReturn MemberFunctionStub(TInstancePtr thisPtr, TArgs... args) + { + TClass* p = static_cast(thisPtr); + return (p->*TFunction)(std::forward(args)...); + } + + template + static TReturn ConstMemberFunctionStub(TInstancePtr thisPtr, TArgs... args) + { + const TClass* p = static_cast(thisPtr); + return (p->*TFunction)(std::forward(args)...); + } + + template + static TReturn FreeFunctionStub(TInstancePtr thisPtr, TArgs... args) + { + return (TFunction)(std::forward(args)...); + } + + template + static TReturn LambdaStub(TInstancePtr thisPtr, TArgs... args) + { + TLambda* p = static_cast(thisPtr); + return (p->operator())(std::forward(args)...); + } + + private: + InvocationElement m_Invocation; + }; + + //========================================================================== + /** Simple multicast function delegate to bind multiple callbacks of some + sort without unnecessary allocations. + */ + template + class MulticastDelegate + { + using TDelegate = Delegate; + using TInstancePtr = typename TDelegate::TInstancePtr; + using TInternalFunction = typename TDelegate::TInternalFunction; + using InvocationElement = typename TDelegate::InvocationElement; + public: + MulticastDelegate() = default; + + MulticastDelegate(const MulticastDelegate& other) { m_InvocationList = other.m_InvocationList; } + MulticastDelegate& operator =(const MulticastDelegate& other) { m_InvocationList = other.m_InvocationList; return *this; } + bool operator == (const MulticastDelegate& other) const { return m_InvocationList == other.m_InvocationList; } + bool operator != (const MulticastDelegate& other) const { return m_InvocationList != other.m_InvocationList; } + + //========================================================================== + /// Free function binding + template + void Bind() + { + Add(nullptr, FreeFunctionStub); + } + + template + void Bind(const TLambda& lambda) + { + Add((TDelegate::TInstancePtr)(&lambda), LambdaStub); + } + + /// Member function binding + template + void Bind(TClass* object) + { + using TMembFunc = TReturn(TClass::*)(TArgs...); + using TMembFuncConst = TReturn(TClass::*)(TArgs...) const; + + if constexpr (std::is_same_v) + { + Add(const_cast(object), ConstMemberFunctionStub); + } + else + { + static_assert(std::is_same_v, "Invalid function signature."); // TODO: C++20 'requires' woul've solved this + Add((TInstancePtr)(object), MemberFunctionStub); + } + } + + //========================================================================== + /// Free function unbinding + template + void Unbind() + { + Remove(nullptr, FreeFunctionStub); + } + + /// Lambda unbinding + template + void Unbind(const TLambda& lambda) + { + Remove((TDelegate::TInstancePtr)(&lambda), LambdaStub); + } + + /// Member function unbinding + template + void Unbind(TClass* object) + { + Remove((TDelegate::TInstancePtr)(object), MemberFunctionStub); + } + + /// Const member function unbinding + template + void Unbind(const TClass* object) + { + Remove(const_cast(object), ConstMemberFunctionStub); + } + + //========================================================================== + bool IsBound() const { return !m_InvocationList.empty(); } + operator bool() const { return IsBound(); } + + /** Multicast Delegate does not support return type handling. */ + void Invoke(TArgs...args) const + { + SE_CORE_ASSERT(IsBound(), "Trying to invoke unbound delegate."); + + // We don't want to Invoke new functions that may be added + // in one of the delegate calls. + // Hopefully none of the elements are removed from the list + // during the iteration. + const uint32_t numberOfInvocations = m_InvocationList.size(); + + uint32_t i = 0; + + for (const auto& element : m_InvocationList) + { + (*(element.Stub))(element.Object, std::forward(args)...); + + if (++i >= numberOfInvocations) + break; + } + } + + private: + inline void Add(TInstancePtr anObject, TInternalFunction aStub) + { + m_InvocationList.push_back(InvocationElement{ anObject, aStub }); + } + + inline void Remove(TInstancePtr anObject, TInternalFunction aStub) + { + m_InvocationList.remove(InvocationElement{ anObject, aStub }); + } + + template + static TReturn MemberFunctionStub(TInstancePtr thisPtr, TArgs... args) + { + TClass* p = static_cast(thisPtr); + return (p->*TFunction)(std::forward(args)...); + } + + template + static TReturn ConstMemberFunctionStub(TInstancePtr thisPtr, TArgs... args) + { + const TClass* p = static_cast(thisPtr); + return (p->*TFunction)(std::forward(args)...); + } + + template + static TReturn FreeFunctionStub(TInstancePtr thisPtr, TArgs... args) + { + return (TFunction)(std::forward(args)...); + } + + template + static TReturn LambdaStub(TInstancePtr thisPtr, TArgs... args) + { + TLambda* p = static_cast(thisPtr); + return (p->operator())(std::forward(args)...); + } + + private: + std::list m_InvocationList; + }; + +} // namespace StarEngine diff --git a/StarEngine/src/StarEngine/Core/EntryPoint.h b/StarEngine/src/StarEngine/Core/EntryPoint.h deleted file mode 100644 index 3ad2086f..00000000 --- a/StarEngine/src/StarEngine/Core/EntryPoint.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include "StarEngine/Core/Base.h" -#include "StarEngine/Core/Application.h" - -#ifdef SE_PLATFORM_WINDOWS - -extern StarEngine::Application* StarEngine::CreateApplication(ApplicationCommandLineArgs args); - - int main(int argc, char** argv) { - StarEngine::Log::Init(); - - SE_PROFILE_BEGIN_SESSION("Startup", "StarEngineProfile-Startup.json"); - auto app = StarEngine::CreateApplication({ argc, argv }); - SE_PROFILE_END_SESSION(); - - SE_PROFILE_BEGIN_SESSION("Runtime", "StarEngineProfile-Runtime.json"); - app->Run(); - SE_PROFILE_END_SESSION(); - - SE_PROFILE_BEGIN_SESSION("Shutdown", "StarEngineProfile-Shutdown.json"); - delete app; - SE_PROFILE_END_SESSION(); - } -#endif diff --git a/StarEngine/src/StarEngine/Core/Events/ApplicationEvent.h b/StarEngine/src/StarEngine/Core/Events/ApplicationEvent.h new file mode 100644 index 00000000..7f3aede4 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Events/ApplicationEvent.h @@ -0,0 +1,102 @@ +#pragma once + +#include "Event.h" + +#include + +namespace StarEngine { + + // TODO: Should this store previous size? + class WindowResizeEvent : public Event + { + public: + WindowResizeEvent(unsigned int width, unsigned int height) + : m_Width(width), m_Height(height) { + } + + inline unsigned int GetWidth() const { return m_Width; } + inline unsigned int GetHeight() const { return m_Height; } + + std::string ToString() const override + { + std::stringstream ss; + ss << "WindowResizeEvent: " << m_Width << ", " << m_Height; + return ss.str(); + } + + EVENT_CLASS_TYPE(WindowResize) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + private: + unsigned int m_Width, m_Height; + }; + + class WindowMinimizeEvent : public Event + { + public: + WindowMinimizeEvent(bool minimized) + : m_Minimized(minimized) { + } + + bool IsMinimized() const { return m_Minimized; } + + EVENT_CLASS_TYPE(WindowMinimize) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + private: + bool m_Minimized = false; + }; + + class WindowCloseEvent : public Event + { + public: + WindowCloseEvent() {} + + EVENT_CLASS_TYPE(WindowClose) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + }; + + class WindowTitleBarHitTestEvent : public Event + { + public: + WindowTitleBarHitTestEvent(int x, int y, int& hit) + : m_X(x), m_Y(y), m_Hit(hit) { + } + + inline int GetX() const { return m_X; } + inline int GetY() const { return m_Y; } + inline void SetHit(bool hit) { m_Hit = (int)hit; } + + EVENT_CLASS_TYPE(WindowTitleBarHitTest) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + private: + int m_X; + int m_Y; + int& m_Hit; + }; + + class AppTickEvent : public Event + { + public: + AppTickEvent() {} + + EVENT_CLASS_TYPE(AppTick) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + }; + + class AppUpdateEvent : public Event + { + public: + AppUpdateEvent() {} + + EVENT_CLASS_TYPE(AppUpdate) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + }; + + class AppRenderEvent : public Event + { + public: + AppRenderEvent() {} + + EVENT_CLASS_TYPE(AppRender) + EVENT_CLASS_CATEGORY(EventCategoryApplication) + }; +} diff --git a/StarEngine/src/StarEngine/Core/Events/EditorEvents.h b/StarEngine/src/StarEngine/Core/Events/EditorEvents.h new file mode 100644 index 00000000..4f39de1a --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Events/EditorEvents.h @@ -0,0 +1,44 @@ +#pragma once + +#include "Event.h" + +#include + +namespace StarEngine { + + class EditorExitPlayModeEvent : public Event + { + public: + EditorExitPlayModeEvent() = default; + + std::string ToString() const override + { + std::stringstream ss; + ss << "EditorExitPlayModeEvent"; + return ss.str(); + } + + EVENT_CLASS_TYPE(EditorExitPlayMode) + EVENT_CLASS_CATEGORY(EventCategoryEditor) + }; + + class AssetReloadedEvent : public Event + { + public: + AssetReloadedEvent(AssetHandle assetHandle) : AssetHandle(assetHandle) {} + + std::string ToString() const override + { + std::stringstream ss; + ss << "AssetReloadedEvent"; + return ss.str(); + } + + EVENT_CLASS_TYPE(AssetReloaded) + EVENT_CLASS_CATEGORY(EventCategoryEditor) + + public: + AssetHandle AssetHandle; + }; + +} diff --git a/StarEngine/src/StarEngine/Events/Event.h b/StarEngine/src/StarEngine/Core/Events/Event.h similarity index 58% rename from StarEngine/src/StarEngine/Events/Event.h rename to StarEngine/src/StarEngine/Core/Events/Event.h index 3541817f..1e51dbae 100644 --- a/StarEngine/src/StarEngine/Events/Event.h +++ b/StarEngine/src/StarEngine/Core/Events/Event.h @@ -1,24 +1,25 @@ #pragma once -#include "StarEngine/Debug/Instrumentor.h" #include "StarEngine/Core/Base.h" #include +#include +#include namespace StarEngine { - // Events in StarEngine are currently blocking, meaning when an event occurs it - // immediately gets dispatched and must be dealt with right then an there. - // For the future, a better strategy might be to buffer events in an event - // bus and process them during the "event" part of the update stage. - enum class EventType { None = 0, - WindowClose, WindowResize, WindowFocus, WindowLostFocus, WindowMoved, WindowDrop, + WindowClose, WindowMinimize, WindowResize, WindowFocus, WindowLostFocus, WindowMoved, WindowTitleBarHitTest, AppTick, AppUpdate, AppRender, KeyPressed, KeyReleased, KeyTyped, - MouseButtonPressed, MouseButtonReleased, MouseMoved, MouseScrolled + MouseButtonPressed, MouseButtonReleased, MouseButtonDown, MouseMoved, MouseScrolled, + ScenePreStart, ScenePostStart, ScenePreStop, ScenePostStop, + EditorExitPlayMode, + SelectionChanged, + AssetReloaded, + AnimationGraphCompiled }; enum EventCategory @@ -28,7 +29,9 @@ namespace StarEngine { EventCategoryInput = BIT(1), EventCategoryKeyboard = BIT(2), EventCategoryMouse = BIT(3), - EventCategoryMouseButton = BIT(4) + EventCategoryMouseButton = BIT(4), + EventCategoryScene = BIT(5), + EventCategoryEditor = BIT(6) }; #define EVENT_CLASS_TYPE(type) static EventType GetStaticType() { return EventType::type; }\ @@ -40,16 +43,16 @@ namespace StarEngine { class Event { public: - virtual ~Event() = default; - bool Handled = false; + bool Synced = false; // Queued events are only processed if this is true. It is set true when asset thread syncs with main thread. + virtual ~Event() {} virtual EventType GetEventType() const = 0; virtual const char* GetName() const = 0; virtual int GetCategoryFlags() const = 0; virtual std::string ToString() const { return GetName(); } - bool IsInCategory(EventCategory category) + inline bool IsInCategory(EventCategory category) { return GetCategoryFlags() & category; } @@ -57,20 +60,20 @@ namespace StarEngine { class EventDispatcher { + template + using EventFn = std::function; public: EventDispatcher(Event& event) : m_Event(event) { - } - // F will be deduced by the compiler - template - bool Dispatch(const F& func) + template + bool Dispatch(EventFn func) { - if (m_Event.GetEventType() == T::GetStaticType()) + if (m_Event.GetEventType() == T::GetStaticType() && !m_Event.Handled) { - m_Event.Handled |= func(static_cast(m_Event)); + m_Event.Handled = func(*(T*)&m_Event); return true; } return false; @@ -83,5 +86,5 @@ namespace StarEngine { { return os << e.ToString(); } - } + diff --git a/StarEngine/src/StarEngine/Events/KeyEvent.h b/StarEngine/src/StarEngine/Core/Events/KeyEvent.h similarity index 67% rename from StarEngine/src/StarEngine/Events/KeyEvent.h rename to StarEngine/src/StarEngine/Core/Events/KeyEvent.h index 742f4399..184e63c0 100644 --- a/StarEngine/src/StarEngine/Events/KeyEvent.h +++ b/StarEngine/src/StarEngine/Core/Events/KeyEvent.h @@ -1,18 +1,20 @@ #pragma once -#include "StarEngine/Events/Event.h" +#include "Event.h" #include "StarEngine/Core/KeyCodes.h" +#include + namespace StarEngine { class KeyEvent : public Event { public: - KeyCode GetKeyCode() const { return m_KeyCode; } + inline KeyCode GetKeyCode() const { return m_KeyCode; } EVENT_CLASS_CATEGORY(EventCategoryKeyboard | EventCategoryInput) protected: - KeyEvent(const KeyCode keycode) + KeyEvent(KeyCode keycode) : m_KeyCode(keycode) { } @@ -22,28 +24,28 @@ namespace StarEngine { class KeyPressedEvent : public KeyEvent { public: - KeyPressedEvent(const KeyCode keycode, bool isRepeat = false) - : KeyEvent(keycode), m_IsRepeat(isRepeat) { + KeyPressedEvent(KeyCode keycode, int repeatCount) + : KeyEvent(keycode), m_RepeatCount(repeatCount) { } - bool IsRepeat() const { return m_IsRepeat; } + inline int GetRepeatCount() const { return m_RepeatCount; } std::string ToString() const override { std::stringstream ss; - ss << "KeyPressedEvent: " << m_KeyCode << " (repeat = " << m_IsRepeat << ")"; + ss << "KeyPressedEvent: " << m_KeyCode << " (" << m_RepeatCount << " repeats)"; return ss.str(); } EVENT_CLASS_TYPE(KeyPressed) private: - bool m_IsRepeat; + int m_RepeatCount; }; class KeyReleasedEvent : public KeyEvent { public: - KeyReleasedEvent(const KeyCode keycode) + KeyReleasedEvent(KeyCode keycode) : KeyEvent(keycode) { } @@ -60,7 +62,7 @@ namespace StarEngine { class KeyTypedEvent : public KeyEvent { public: - KeyTypedEvent(const KeyCode keycode) + KeyTypedEvent(KeyCode keycode) : KeyEvent(keycode) { } diff --git a/StarEngine/src/StarEngine/Events/MouseEvent.h b/StarEngine/src/StarEngine/Core/Events/MouseEvent.h similarity index 60% rename from StarEngine/src/StarEngine/Events/MouseEvent.h rename to StarEngine/src/StarEngine/Core/Events/MouseEvent.h index 0a6d83a9..b024cb4c 100644 --- a/StarEngine/src/StarEngine/Events/MouseEvent.h +++ b/StarEngine/src/StarEngine/Core/Events/MouseEvent.h @@ -1,19 +1,22 @@ #pragma once -#include "StarEngine/Events/Event.h" -#include "StarEngine/Core/MouseCodes.h" +#include "Event.h" + +#include + +#include "StarEngine/Core/KeyCodes.h" namespace StarEngine { class MouseMovedEvent : public Event { public: - MouseMovedEvent(const float x, const float y) + MouseMovedEvent(float x, float y) : m_MouseX(x), m_MouseY(y) { } - float GetX() const { return m_MouseX; } - float GetY() const { return m_MouseY; } + inline float GetX() const { return m_MouseX; } + inline float GetY() const { return m_MouseY; } std::string ToString() const override { @@ -31,12 +34,12 @@ namespace StarEngine { class MouseScrolledEvent : public Event { public: - MouseScrolledEvent(const float xOffset, const float yOffset) + MouseScrolledEvent(float xOffset, float yOffset) : m_XOffset(xOffset), m_YOffset(yOffset) { } - float GetXOffset() const { return m_XOffset; } - float GetYOffset() const { return m_YOffset; } + inline float GetXOffset() const { return m_XOffset; } + inline float GetYOffset() const { return m_YOffset; } std::string ToString() const override { @@ -54,21 +57,21 @@ namespace StarEngine { class MouseButtonEvent : public Event { public: - MouseCode GetMouseButton() const { return m_Button; } + inline MouseButton GetMouseButton() const { return m_Button; } - EVENT_CLASS_CATEGORY(EventCategoryMouse | EventCategoryInput | EventCategoryMouseButton) + EVENT_CLASS_CATEGORY(EventCategoryMouse | EventCategoryInput) protected: - MouseButtonEvent(const MouseCode button) + MouseButtonEvent(MouseButton button) : m_Button(button) { } - MouseCode m_Button; + MouseButton m_Button; }; class MouseButtonPressedEvent : public MouseButtonEvent { public: - MouseButtonPressedEvent(const MouseCode button) + MouseButtonPressedEvent(MouseButton button) : MouseButtonEvent(button) { } @@ -85,7 +88,7 @@ namespace StarEngine { class MouseButtonReleasedEvent : public MouseButtonEvent { public: - MouseButtonReleasedEvent(const MouseCode button) + MouseButtonReleasedEvent(MouseButton button) : MouseButtonEvent(button) { } @@ -99,4 +102,21 @@ namespace StarEngine { EVENT_CLASS_TYPE(MouseButtonReleased) }; + class MouseButtonDownEvent : public MouseButtonEvent + { + public: + MouseButtonDownEvent(MouseButton button) + : MouseButtonEvent(button) + { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "MouseButtonDownEvent: " << m_Button; + return ss.str(); + } + + EVENT_CLASS_TYPE(MouseButtonDown) + }; } diff --git a/StarEngine/src/StarEngine/Core/Events/SceneEvents.h b/StarEngine/src/StarEngine/Core/Events/SceneEvents.h new file mode 100644 index 00000000..11996040 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Events/SceneEvents.h @@ -0,0 +1,122 @@ +#pragma once + +#include "Event.h" +#include "StarEngine/Scene/Scene.h" +#include "StarEngine/Editor/SelectionManager.h" + +#include + +namespace StarEngine { + + class SceneEvent : public Event + { + public: + const Ref& GetScene() const { return m_Scene; } + Ref GetScene() { return m_Scene; } + + EVENT_CLASS_CATEGORY(EventCategoryApplication | EventCategoryScene) + protected: + SceneEvent(const Ref& scene) + : m_Scene(scene) { + } + + Ref m_Scene; + }; + + class ScenePreStartEvent : public SceneEvent + { + public: + ScenePreStartEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePreStartEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePreStart) + }; + + class ScenePostStartEvent : public SceneEvent + { + public: + ScenePostStartEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePostStartEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePostStart) + }; + + class ScenePreStopEvent : public SceneEvent + { + public: + ScenePreStopEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePreStopEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePreStop) + }; + + class ScenePostStopEvent : public SceneEvent + { + public: + ScenePostStopEvent(const Ref& scene) + : SceneEvent(scene) { + } + + std::string ToString() const override + { + std::stringstream ss; + ss << "ScenePostStopEvent: " << m_Scene->GetName(); + return ss.str(); + } + + EVENT_CLASS_TYPE(ScenePostStop) + }; + + // TODO: Probably move this somewhere else... + class SelectionChangedEvent : public Event + { + public: + SelectionChangedEvent(SelectionContext contextID, UUID selectionID, bool selected) + : m_Context(contextID), m_SelectionID(selectionID), m_Selected(selected) + { + } + + SelectionContext GetContextID() const { return m_Context; } + UUID GetSelectionID() const { return m_SelectionID; } + bool IsSelected() const { return m_Selected; } + + std::string ToString() const override + { + std::stringstream ss; + ss << "EntitySelectionChangedEvent: Context(" << (int32_t)m_Context << "), Selection(" << m_SelectionID << "), " << m_Selected; + return ss.str(); + } + + EVENT_CLASS_CATEGORY(EventCategoryScene) + EVENT_CLASS_TYPE(SelectionChanged) + private: + SelectionContext m_Context; + UUID m_SelectionID; + bool m_Selected; + }; + +} diff --git a/StarEngine/src/StarEngine/Core/FastRandom.h b/StarEngine/src/StarEngine/Core/FastRandom.h new file mode 100644 index 00000000..fb94695f --- /dev/null +++ b/StarEngine/src/StarEngine/Core/FastRandom.h @@ -0,0 +1,317 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace StarEngine { + + // High-performance PCG32 random number generator + class FastRandom + { + public: + using result_type = uint32_t; + static constexpr result_type min() { return 0; } + static constexpr result_type max() { return UINT32_MAX; } + + FastRandom() noexcept : m_State(DEFAULT_SEED), m_Inc(DEFAULT_INC) {} + + explicit FastRandom(uint64_t seed) noexcept + { + SetSeed(seed); + } + + FastRandom(uint64_t seed, uint64_t sequence) noexcept + { + SetSeed(seed, sequence); + } + + void SetSeed(uint64_t seed) noexcept + { + SetSeed(seed, DEFAULT_INC); + } + + void SetSeed(uint64_t seed, uint64_t sequence) noexcept + { + m_State = 0U; + m_Inc = (sequence << 1u) | 1u; + NextUInt32(); + m_State += seed; + NextUInt32(); + } + + uint64_t GetCurrentSeed() const noexcept { return m_State; } + + uint32_t operator()() noexcept { return NextUInt32(); } + + uint32_t NextUInt32() noexcept + { + uint64_t oldstate = m_State; + m_State = oldstate * PCG_MULTIPLIER + m_Inc; + uint32_t xorshifted = static_cast(((oldstate >> 18u) ^ oldstate) >> 27u); + uint32_t rot = static_cast(oldstate >> 59u); + return (xorshifted >> rot) | (xorshifted << ((-rot) & 31)); + } + + int32_t NextInt32() noexcept + { + return static_cast(NextUInt32()); + } + + uint16_t NextUInt16() noexcept + { + return static_cast(NextUInt32() >> 16); + } + + int16_t NextInt16() noexcept + { + return static_cast(NextUInt16()); + } + + uint8_t NextUInt8() noexcept + { + return static_cast(NextUInt32() >> 24); + } + + int8_t NextInt8() noexcept + { + return static_cast(NextUInt8()); + } + + float NextFloat() noexcept + { + return static_cast(NextUInt32() >> 8) * FLOAT_DIVISOR; + } + + double NextDouble() noexcept + { + uint64_t high = static_cast(NextUInt32()) << 21; + uint64_t low = NextUInt32() >> 11; + return static_cast(high | low) * DOUBLE_DIVISOR; + } + + template + T NextInRange(T min_val, T max_val) noexcept + { + static_assert(std::is_arithmetic_v, "T must be arithmetic type"); + + if constexpr (std::is_integral_v) + { + return NextIntInRange(min_val, max_val); + } + else + { + return NextFloatInRange(min_val, max_val); + } + } + + int32_t NextIntInRange(int32_t min_val, int32_t max_val) noexcept + { + if (min_val >= max_val) return min_val; + + uint32_t range = static_cast(max_val - min_val); + uint32_t threshold = (UINT32_MAX - range + 1) % (range + 1); + + uint32_t result; + do { + result = NextUInt32(); + } while (result < threshold); + + return min_val + static_cast(result % (range + 1)); + } + + float NextFloatInRange(float min_val, float max_val) noexcept + { + return min_val + NextFloat() * (max_val - min_val); + } + + double NextDoubleInRange(double min_val, double max_val) noexcept + { + return min_val + NextDouble() * (max_val - min_val); + } + + bool NextBool() noexcept + { + return (NextUInt32() & 1) != 0; + } + + bool NextBool(float probability) noexcept + { + return NextFloat() < probability; + } + + // Convenience methods (legacy compatibility) + uint32_t GetUInt32() noexcept { return NextUInt32(); } + int32_t GetInt32() noexcept { return NextInt32(); } + float GetFloat32() noexcept { return NextFloat(); } + double GetFloat64() noexcept { return NextDouble(); } + + template + T GetInRange(T min_val, T max_val) noexcept { return NextInRange(min_val, max_val); } + + float NextGaussian(float mean = 0.0f, float stddev = 1.0f) noexcept + { + static bool hasSpare = false; + static float spare; + + if (hasSpare) + { + hasSpare = false; + return spare * stddev + mean; + } + + hasSpare = true; + float u = NextFloat(); + float v = NextFloat(); + float mag = stddev * std::sqrt(-2.0f * std::log(u)); + spare = mag * std::cos(2.0f * PI * v); + return mag * std::sin(2.0f * PI * v) + mean; + } + + template + void Shuffle(Iterator first, Iterator last) noexcept + { + auto n = std::distance(first, last); + for (auto i = n - 1; i > 0; --i) + { + auto j = NextIntInRange(0, static_cast(i)); + std::swap(*(first + i), *(first + j)); + } + } + + template + void Shuffle(Container& container) noexcept + { + Shuffle(container.begin(), container.end()); + } + + private: + uint64_t m_State; + uint64_t m_Inc; + + static constexpr uint64_t PCG_MULTIPLIER = 6364136223846793005ULL; + static constexpr uint64_t DEFAULT_SEED = 0x853c49e6748fea9bULL; + static constexpr uint64_t DEFAULT_INC = 0xda3e39cb94b95bdbULL; + + static constexpr float FLOAT_DIVISOR = 1.0f / 16777216.0f; // 1 / 2^24 + static constexpr double DOUBLE_DIVISOR = 1.0 / 9007199254740992.0; // 1 / 2^53 + static constexpr float PI = 3.14159265358979323846f; + }; + + // Ultra-fast Xoshiro256++ generator for when maximum speed is needed + class UltraFastRandom + { + public: + using result_type = uint64_t; + static constexpr result_type min() { return 0; } + static constexpr result_type max() { return UINT64_MAX; } + + UltraFastRandom() noexcept + { + SetSeed(static_cast( + std::chrono::high_resolution_clock::now().time_since_epoch().count() + )); + } + + explicit UltraFastRandom(uint64_t seed) noexcept + { + SetSeed(seed); + } + + void SetSeed(uint64_t seed) noexcept + { + auto splitmix64 = [](uint64_t& z) { + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL; + return z ^ (z >> 31); + }; + + uint64_t z = seed; + m_State[0] = splitmix64(z); + m_State[1] = splitmix64(z); + m_State[2] = splitmix64(z); + m_State[3] = splitmix64(z); + } + + uint64_t operator()() noexcept { return NextUInt64(); } + + uint64_t NextUInt64() noexcept + { + const uint64_t result = RotLeft(m_State[0] + m_State[3], 23) + m_State[0]; + const uint64_t t = m_State[1] << 17; + + m_State[2] ^= m_State[0]; + m_State[3] ^= m_State[1]; + m_State[1] ^= m_State[2]; + m_State[0] ^= m_State[3]; + + m_State[2] ^= t; + m_State[3] = RotLeft(m_State[3], 45); + + return result; + } + + uint32_t NextUInt32() noexcept + { + return static_cast(NextUInt64() >> 32); + } + + float NextFloat() noexcept + { + return static_cast(NextUInt32() >> 8) * (1.0f / 16777216.0f); + } + + template + T NextInRange(T min_val, T max_val) noexcept + { + if constexpr (std::is_integral_v) + { + if (min_val >= max_val) return min_val; + uint64_t range = static_cast(max_val - min_val); + return min_val + static_cast(NextUInt64() % (range + 1)); + } + else + { + return min_val + NextFloat() * (max_val - min_val); + } + } + + private: + uint64_t m_State[4]; + + static constexpr uint64_t RotLeft(uint64_t x, int k) noexcept + { + return (x << k) | (x >> (64 - k)); + } + }; + + // Utility functions for seeding and common operations + namespace Random + { + inline uint64_t GetSeedFromCurrentTime() noexcept + { + return static_cast( + std::chrono::high_resolution_clock::now().time_since_epoch().count() + ); + } + + inline thread_local FastRandom g_Random; + inline thread_local UltraFastRandom g_UltraFastRandom; + + inline uint32_t RandomUInt32() { return g_Random.NextUInt32(); } + inline float RandomFloat() { return g_Random.NextFloat(); } + inline bool RandomBool() { return g_Random.NextBool(); } + + template + inline T RandomInRange(T min_val, T max_val) { return g_Random.NextInRange(min_val, max_val); } + + inline void SetGlobalSeed(uint64_t seed) + { + g_Random.SetSeed(seed); + g_UltraFastRandom.SetSeed(seed); + } + } + +} diff --git a/StarEngine/src/StarEngine/Core/FatalSignal.cpp b/StarEngine/src/StarEngine/Core/FatalSignal.cpp new file mode 100644 index 00000000..e59099cb --- /dev/null +++ b/StarEngine/src/StarEngine/Core/FatalSignal.cpp @@ -0,0 +1,102 @@ +#include "sepch.h" + +#include "FatalSignal.h" + +#ifndef SE_PLATFORM_WINDOWS +#include +#endif + +#include + +namespace StarEngine { + FatalSignal FatalSignal::s_State; + + void FatalSignal::Die() { + auto st = backward::StackTrace(); + st.load_here(32); + backward::Printer().print(st); + + _Exit(-1); // Exit immediately with no more process entry. + } + + void FatalSignal::Timeout() { + puts("FATAL SIGNAL TIMEOUT"); + Die(); + } + + void FatalSignal::Handler(const char* what) { + if(m_Active) { + puts("NESTED ERROR STATE"); // Safest output mode we can muster fn. + puts(what); + Die(); + } + + puts("FATAL SIGNAL RECEIVED"); + puts(what); + + m_Active = true; + +#ifndef SE_PLATFORM_WINDOWS + ualarm(m_Timeout * 1000, 0); +#else + std::thread t([&] { + auto dur = std::chrono::duration(m_Timeout); + std::this_thread::sleep_for(dur); + Timeout(); + }); +#endif + + for(auto& fn : m_Callbacks) fn(); + + Die(); + } + + void FatalSignal::Install(long timeout) { + s_State.m_Timeout = timeout; + + std::set_terminate([] { + auto eptr = std::current_exception(); + const char* what = ""; + try { + if(eptr) std::rethrow_exception(eptr); + } + catch(const std::exception& e) { + what = e.what(); + } + s_State.Handler(what); + }); + + // Technically using most of the stdlib is UB in many of these signals' + // Cases but most implementations allow a lot more of it to be used. + // Worst case scenario is we just don't get a chance to do our desired + // Cleanup -- but we should still be able to do enough file io and + // State reads to emit an emergency save. + void (*sig)(int) = [] (int sig) { + const char* name = ""; + switch(sig) { + case SIGABRT: name = "SIGABRT"; break; + case SIGFPE: name = "SIGFPE"; break; + case SIGILL: name = "SIGILL"; break; + case SIGINT: name = "SIGINT"; break; + case SIGSEGV: name = "SIGSEGV"; break; + case SIGTERM: name = "SIGTERM"; break; + } + s_State.Handler(name); + }; + + signal(SIGABRT, sig); + signal(SIGFPE, sig); + signal(SIGILL, sig); + signal(SIGINT, sig); + signal(SIGSEGV, sig); + signal(SIGTERM, sig); + +#ifndef SE_PLATFORM_WINDOWS + signal(SIGALRM, [] (int _) { Timeout(); }); +#endif + } + + void FatalSignal::Add(ProcFn& fn) { + s_State.m_Callbacks.emplace_back(fn); + } +} diff --git a/StarEngine/src/StarEngine/Core/FatalSignal.h b/StarEngine/src/StarEngine/Core/FatalSignal.h new file mode 100644 index 00000000..07da56f0 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/FatalSignal.h @@ -0,0 +1,48 @@ +#pragma once + +namespace StarEngine { + /// TODO(Emily): Include a backtrace lib so we can get nice native + /// Stacktraces when something goes wrong even without a + /// Debugger attached. + + /// + /// Tries to catch as many fatal exit states and give the engine a chance + /// To save unsaved data etc. + /// + /// This is NOT for recoverable failure states -- the operating env. is in + /// A largely undefined state after a fatal signal and it may not be safe + /// To resume program flow. It also can make it difficult for user-provided + /// Kill signals to actually end the program if they need to force-kill it. + /// + /// Please try and avoid using spdlog from within fatal callback functions + /// As it has a habit of ending up with messed up sink states during fatal + /// Shutdown. + /// + /// If a callback is taking too long or a nested failure state occurs -- + /// `FatalSignal` will force-shutdown the process. + /// + class FatalSignal { + private: + using Proc = void(); + using ProcFn = std::function; + + static FatalSignal s_State; + + std::vector m_Callbacks; + long m_Timeout; // Timeout in milliseconds. + bool m_Active = false; + + void Handler(const char* what); + + static void Timeout(); + static void Terminate(); + + public: + // Kill the program immediately. Do not run any handlers or fallbacks. + static void Die(); + + static void Install(long timeout = 2000); + + static void Add(ProcFn& fn); + }; +} diff --git a/StarEngine/src/StarEngine/Core/FileSystem.cpp b/StarEngine/src/StarEngine/Core/FileSystem.cpp deleted file mode 100644 index dec402eb..00000000 --- a/StarEngine/src/StarEngine/Core/FileSystem.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "sepch.h" -#include "FileSystem.h" - -namespace StarEngine { - - Buffer FileSystem::ReadFileBinary(const std::filesystem::path& filepath) - { - std::ifstream stream(filepath, std::ios::binary | std::ios::ate); - - if (!stream) - { - // Failed to open the file - return {}; - } - - - std::streampos end = stream.tellg(); - stream.seekg(0, std::ios::beg); - uint64_t size = end - stream.tellg(); - - if (size == 0) - { - // File is empty - return {}; - } - - Buffer buffer(size); - stream.read(buffer.As(), size); - stream.close(); - return buffer; - } - -} diff --git a/StarEngine/src/StarEngine/Core/FileSystem.h b/StarEngine/src/StarEngine/Core/FileSystem.h deleted file mode 100644 index 05ee09d9..00000000 --- a/StarEngine/src/StarEngine/Core/FileSystem.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "StarEngine/Core/Buffer.h" - -namespace StarEngine { - - class FileSystem - { - public: - // TODO: move to FileSystem class - static Buffer ReadFileBinary(const std::filesystem::path& filepath); - }; - -} diff --git a/StarEngine/src/StarEngine/Asset/RuntimeAssetManager.cpp b/StarEngine/src/StarEngine/Core/Hash.cpp similarity index 63% rename from StarEngine/src/StarEngine/Asset/RuntimeAssetManager.cpp rename to StarEngine/src/StarEngine/Core/Hash.cpp index a7248558..d2430879 100644 --- a/StarEngine/src/StarEngine/Asset/RuntimeAssetManager.cpp +++ b/StarEngine/src/StarEngine/Core/Hash.cpp @@ -1,7 +1,6 @@ #include "sepch.h" -#include "AssetManager.h" +#include "Hash.h" namespace StarEngine { - } diff --git a/StarEngine/src/StarEngine/Core/Hash.h b/StarEngine/src/StarEngine/Core/Hash.h new file mode 100644 index 00000000..a800388e --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Hash.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace StarEngine { + + class Hash + { + public: + static constexpr uint32_t GenerateFNVHash(std::string_view str) + { + constexpr uint32_t FNV_PRIME = 16777619u; + constexpr uint32_t OFFSET_BASIS = 2166136261u; + + const size_t length = str.length(); + const char* data = str.data(); + + uint32_t hash = OFFSET_BASIS; + for (size_t i = 0; i < length; ++i) + { + hash ^= *data++; + hash *= FNV_PRIME; + } + hash ^= '\0'; + hash *= FNV_PRIME; + + return hash; + } + + static uint32_t CRC32(const char* str); + static uint32_t CRC32(const std::string& string); + }; + +} diff --git a/StarEngine/src/StarEngine/Core/HashCRC32.cpp b/StarEngine/src/StarEngine/Core/HashCRC32.cpp new file mode 100644 index 00000000..38a452e5 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/HashCRC32.cpp @@ -0,0 +1,55 @@ +#include +#include "Hash.h" + +constexpr auto gen_crc32_table() +{ + constexpr int num_bytes = 256; + constexpr int num_iterations = 8; + constexpr uint32_t polynomial = 0xEDB88320; + + std::array crc32_table{}; + + for (int byte = 0; byte < num_bytes; ++byte) + { + uint32_t crc = (uint32_t)byte; + for (int i = 0; i < num_iterations; ++i) + { + int mask = -((int)crc & 1); + crc = (crc >> 1) ^ (polynomial & mask); + } + + crc32_table[byte] = crc; + } + + return crc32_table; +} + +static constexpr auto crc32_table = gen_crc32_table(); +static_assert( + crc32_table.size() == 256 && + crc32_table[1] == 0x77073096 && + crc32_table[255] == 0x2D02EF8D, + "gen_crc32_table generated unexpected result." + ); + + +namespace StarEngine +{ + + uint32_t Hash::CRC32(const char* str) + { + auto crc = 0xFFFFFFFFu; + + for (auto i = 0u; auto c = str[i]; ++i) { + crc = crc32_table[(crc ^ c) & 0xFF] ^ (crc >> 8); + } + + return ~crc; + } + + uint32_t Hash::CRC32(const std::string& string) + { + return CRC32(string.c_str()); + } + +} diff --git a/StarEngine/src/StarEngine/Core/Input.cpp b/StarEngine/src/StarEngine/Core/Input.cpp new file mode 100644 index 00000000..1b0415c5 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Input.cpp @@ -0,0 +1,378 @@ +#include "sepch.h" +#include "StarEngine/Core/Input.h" +#include "Window.h" + +#include "StarEngine/Core/Application.h" +#include "StarEngine/ImGui/PropertyGrid.h" + +#include +#include + +namespace StarEngine { + + void Input::Update() + { + // Cleanup disconnected controller + for (auto it = s_Controllers.begin(); it != s_Controllers.end(); ) + { + int id = it->first; + if (glfwJoystickPresent(id) != GLFW_TRUE) + it = s_Controllers.erase(it); + else + it++; + } + + // Update controllers + for (int id = GLFW_JOYSTICK_1; id < GLFW_JOYSTICK_LAST; id++) + { + if (glfwJoystickPresent(id) == GLFW_TRUE) + { + Controller& controller = s_Controllers[id]; + controller.ID = id; + controller.Name = glfwGetJoystickName(id); + + int buttonCount; + const unsigned char* buttons = glfwGetJoystickButtons(id, &buttonCount); + for (int i = 0; i < buttonCount; i++) + { + if (buttons[i] == GLFW_PRESS && !controller.ButtonDown[i]) + controller.ButtonStates[i].State = KeyState::Pressed; + else if (buttons[i] == GLFW_RELEASE && controller.ButtonDown[i]) + controller.ButtonStates[i].State = KeyState::Released; + + controller.ButtonDown[i] = buttons[i] == GLFW_PRESS; + } + + int axisCount; + const float* axes = glfwGetJoystickAxes(id, &axisCount); + for (int i = 0; i < axisCount; i++) + controller.AxisStates[i] = abs(axes[i]) > controller.DeadZones[i] ? axes[i] : 0.0f; + + int hatCount; + const unsigned char* hats = glfwGetJoystickHats(id, &hatCount); + for (int i = 0; i < hatCount; i++) + controller.HatStates[i] = hats[i]; + } + } + } + + bool Input::IsKeyPressed(KeyCode key) + { + return s_KeyData.find(key) != s_KeyData.end() && s_KeyData[key].State == KeyState::Pressed; + } + + bool Input::IsKeyHeld(KeyCode key) + { + return s_KeyData.find(key) != s_KeyData.end() && s_KeyData[key].State == KeyState::Held; + } + + bool Input::IsKeyDown(KeyCode keycode) + { + bool enableImGui = Application::Get().GetSpecification().EnableImGui; + if (!enableImGui) + { + auto& window = static_cast(Application::Get().GetWindow()); + auto state = glfwGetKey(static_cast(window.GetNativeWindow()), static_cast(keycode)); + return state == GLFW_PRESS || state == GLFW_REPEAT; + } + + auto& window = static_cast(Application::Get().GetWindow()); + GLFWwindow* win = static_cast(window.GetNativeWindow()); + ImGuiContext* context = ImGui::GetCurrentContext(); + bool pressed = false; + for (ImGuiViewport* viewport : context->Viewports) + { + if (!viewport->PlatformUserData) + continue; + + GLFWwindow* windowHandle = *(GLFWwindow**)viewport->PlatformUserData; // First member is GLFWwindow + if (!windowHandle) + continue; + auto state = glfwGetKey(windowHandle, static_cast(keycode)); + if (state == GLFW_PRESS || state == GLFW_REPEAT) + { + pressed = true; + break; + } + } + + return pressed; + } + + bool Input::IsKeyReleased(KeyCode key) + { + return s_KeyData.find(key) != s_KeyData.end() && s_KeyData[key].State == KeyState::Released; + } + + bool Input::IsMouseButtonPressed(MouseButton button) + { + return s_MouseData.find(button) != s_MouseData.end() && s_MouseData[button].State == KeyState::Pressed; + } + + bool Input::IsMouseButtonHeld(MouseButton button) + { + return s_MouseData.find(button) != s_MouseData.end() && s_MouseData[button].State == KeyState::Held; + } + + bool Input::IsMouseButtonDown(MouseButton button) + { + bool enableImGui = Application::Get().GetSpecification().EnableImGui; + if (!enableImGui) + { + auto& window = static_cast(Application::Get().GetWindow()); + auto state = glfwGetMouseButton(static_cast(window.GetNativeWindow()), static_cast(button)); + return state == GLFW_PRESS; + } + + ImGuiContext* context = ImGui::GetCurrentContext(); + bool pressed = false; + for (ImGuiViewport* viewport : context->Viewports) + { + if (!viewport->PlatformUserData) + continue; + + GLFWwindow* windowHandle = *(GLFWwindow**)viewport->PlatformUserData; // First member is GLFWwindow + if (!windowHandle) + continue; + + auto state = glfwGetMouseButton(static_cast(windowHandle), static_cast(button)); + if (state == GLFW_PRESS || state == GLFW_REPEAT) + { + pressed = true; + break; + } + } + return pressed; + } + + bool Input::IsMouseButtonReleased(MouseButton button) + { + return s_MouseData.find(button) != s_MouseData.end() && s_MouseData[button].State == KeyState::Released; + } + + float Input::GetMouseX() + { + auto [x, y] = GetMousePosition(); + return (float)x; + } + + float Input::GetMouseY() + { + auto [x, y] = GetMousePosition(); + return (float)y; + } + + std::pair Input::GetMousePosition() + { + auto& window = static_cast(Application::Get().GetWindow()); + + double x, y; + glfwGetCursorPos(static_cast(window.GetNativeWindow()), &x, &y); + return { (float)x, (float)y }; + } + + // TODO: A better way to do this is to handle it internally, and simply move the cursor the opposite side + // of the screen when it reaches the edge + void Input::SetCursorMode(CursorMode mode) + { + auto& window = static_cast(Application::Get().GetWindow()); + glfwSetInputMode(static_cast(window.GetNativeWindow()), GLFW_CURSOR, GLFW_CURSOR_NORMAL + (int)mode); + + if (Application::Get().GetSpecification().EnableImGui) + UI::SetInputEnabled(mode == CursorMode::Normal); + } + + CursorMode Input::GetCursorMode() + { + auto& window = static_cast(Application::Get().GetWindow()); + return (CursorMode)(glfwGetInputMode(static_cast(window.GetNativeWindow()), GLFW_CURSOR) - GLFW_CURSOR_NORMAL); + } + + bool Input::IsControllerPresent(int id) + { + return s_Controllers.find(id) != s_Controllers.end(); + } + + std::vector Input::GetConnectedControllerIDs() + { + std::vector ids; + ids.reserve(s_Controllers.size()); + for (auto [id, controller] : s_Controllers) + ids.emplace_back(id); + + return ids; + } + + const Controller* Input::GetController(int id) + { + if (!Input::IsControllerPresent(id)) + return nullptr; + + return &s_Controllers.at(id); + } + + std::string_view Input::GetControllerName(int id) + { + if (!Input::IsControllerPresent(id)) + return {}; + + return s_Controllers.at(id).Name; + } + + bool Input::IsControllerButtonPressed(int controllerID, int button) + { + if (!Input::IsControllerPresent(controllerID)) + return false; + + auto& contoller = s_Controllers.at(controllerID); + return contoller.ButtonStates.find(button) != contoller.ButtonStates.end() && contoller.ButtonStates[button].State == KeyState::Pressed; + } + + bool Input::IsControllerButtonHeld(int controllerID, int button) + { + if (!Input::IsControllerPresent(controllerID)) + return false; + + auto& contoller = s_Controllers.at(controllerID); + return contoller.ButtonStates.find(button) != contoller.ButtonStates.end() && contoller.ButtonStates[button].State == KeyState::Held; + } + + bool Input::IsControllerButtonDown(int controllerID, int button) + { + if (!Input::IsControllerPresent(controllerID)) + return false; + + const Controller& controller = s_Controllers.at(controllerID); + if (controller.ButtonDown.find(button) == controller.ButtonDown.end()) + return false; + + return controller.ButtonDown.at(button); + } + + bool Input::IsControllerButtonReleased(int controllerID, int button) + { + if (!Input::IsControllerPresent(controllerID)) + return true; + + auto& contoller = s_Controllers.at(controllerID); + return contoller.ButtonStates.find(button) != contoller.ButtonStates.end() && contoller.ButtonStates[button].State == KeyState::Released; + } + + float Input::GetControllerAxis(int controllerID, int axis) + { + if (!Input::IsControllerPresent(controllerID)) + return 0.0f; + + const Controller& controller = s_Controllers.at(controllerID); + if (controller.AxisStates.find(axis) == controller.AxisStates.end()) + return 0.0f; + + return controller.AxisStates.at(axis); + } + + uint8_t Input::GetControllerHat(int controllerID, int hat) + { + if (!Input::IsControllerPresent(controllerID)) + return 0; + + const Controller& controller = s_Controllers.at(controllerID); + if (controller.HatStates.find(hat) == controller.HatStates.end()) + return 0; + + return controller.HatStates.at(hat); + } + + float Input::GetControllerDeadzone(int controllerID, int axis) + { + if (!Input::IsControllerPresent(controllerID)) + return 0.0f; + + const Controller& controller = s_Controllers.at(controllerID); + return controller.DeadZones.at(axis); + } + + void Input::SetControllerDeadzone(int controllerID, int axis, float deadzone) + { + if (!Input::IsControllerPresent(controllerID)) + return; + + Controller& controller = s_Controllers.at(controllerID); + controller.DeadZones[axis] = deadzone; + } + + void Input::TransitionPressedKeys() + { + for (const auto& [key, keyData] : s_KeyData) + { + if (keyData.State == KeyState::Pressed) + UpdateKeyState(key, KeyState::Held); + } + + } + + void Input::TransitionPressedButtons() + { + for (const auto& [button, buttonData] : s_MouseData) + { + if (buttonData.State == KeyState::Pressed) + UpdateButtonState(button, KeyState::Held); + } + + for (const auto& [id, controller] : s_Controllers) + { + for (const auto& [button, buttonStates] : controller.ButtonStates) + { + if (buttonStates.State == KeyState::Pressed) + UpdateControllerButtonState(id, button, KeyState::Held); + } + } + } + + void Input::UpdateKeyState(KeyCode key, KeyState newState) + { + auto& keyData = s_KeyData[key]; + keyData.Key = key; + keyData.OldState = keyData.State; + keyData.State = newState; + } + + void Input::UpdateButtonState(MouseButton button, KeyState newState) + { + auto& mouseData = s_MouseData[button]; + mouseData.Button = button; + mouseData.OldState = mouseData.State; + mouseData.State = newState; + } + + void Input::UpdateControllerButtonState(int controllerID, int button, KeyState newState) + { + auto& controllerButtonData = s_Controllers.at(controllerID).ButtonStates.at(button); + controllerButtonData.Button = button; + controllerButtonData.OldState = controllerButtonData.State; + controllerButtonData.State = newState; + } + + void Input::ClearReleasedKeys() + { + for (const auto& [key, keyData] : s_KeyData) + { + if (keyData.State == KeyState::Released) + UpdateKeyState(key, KeyState::None); + } + + for (const auto& [button, buttonData] : s_MouseData) + { + if (buttonData.State == KeyState::Released) + UpdateButtonState(button, KeyState::None); + } + + for (const auto& [id, controller] : s_Controllers) + { + for (const auto& [button, buttonStates] : controller.ButtonStates) + { + if (buttonStates.State == KeyState::Released) + UpdateControllerButtonState(id, button, KeyState::None); + } + } + } +} diff --git a/StarEngine/src/StarEngine/Core/Input.h b/StarEngine/src/StarEngine/Core/Input.h index 15d5e5b2..7725b701 100644 --- a/StarEngine/src/StarEngine/Core/Input.h +++ b/StarEngine/src/StarEngine/Core/Input.h @@ -1,22 +1,97 @@ #pragma once -#include "StarEngine/Core/KeyCodes.h" -#include "StarEngine/Core/MouseCodes.h" +#include "KeyCodes.h" -#include +#include +#include +#include namespace StarEngine { + struct ControllerButtonData + { + int Button; + KeyState State = KeyState::None; + KeyState OldState = KeyState::None; + }; + + struct Controller + { + int ID; + std::string Name; + std::map ButtonDown; + std::map ButtonStates; + std::map AxisStates; + std::map DeadZones; + std::map HatStates; + }; + + struct KeyData + { + KeyCode Key; + KeyState State = KeyState::None; + KeyState OldState = KeyState::None; + }; + + struct ButtonData + { + MouseButton Button; + KeyState State = KeyState::None; + KeyState OldState = KeyState::None; + }; + + class Input { public: - static bool IsKeyPressed(KeyCode key); - static bool IsMouseButtonPressed(MouseCode button); + static void Update(); - static glm::vec2 GetMousePosition(); + static bool IsKeyPressed(KeyCode keycode); + static bool IsKeyHeld(KeyCode keycode); + static bool IsKeyDown(KeyCode keycode); + static bool IsKeyReleased(KeyCode keycode); + static bool IsMouseButtonPressed(MouseButton button); + static bool IsMouseButtonHeld(MouseButton button); + static bool IsMouseButtonDown(MouseButton button); + static bool IsMouseButtonReleased(MouseButton button); static float GetMouseX(); static float GetMouseY(); + static std::pair GetMousePosition(); + + static void SetCursorMode(CursorMode mode); + static CursorMode GetCursorMode(); + + // Controllers + static bool IsControllerPresent(int id); + static std::vector GetConnectedControllerIDs(); + static const Controller* GetController(int id); + static std::string_view GetControllerName(int id); + + static bool IsControllerButtonPressed(int controllerID, int button); + static bool IsControllerButtonHeld(int controllerID, int button); + static bool IsControllerButtonDown(int controllerID, int button); + static bool IsControllerButtonReleased(int controllerID, int button); + + static float GetControllerAxis(int controllerID, int axis); + static uint8_t GetControllerHat(int controllerID, int hat); + + static float GetControllerDeadzone(int controllerID, int axis); + static void SetControllerDeadzone(int controllerID, int axis, float deadzone); + + static const std::map& GetControllers() { return s_Controllers; } + + // Internal use only... + static void TransitionPressedKeys(); + static void TransitionPressedButtons(); + static void UpdateKeyState(KeyCode key, KeyState newState); + static void UpdateButtonState(MouseButton button, KeyState newState); + static void UpdateControllerButtonState(int controller, int button, KeyState newState); + static void ClearReleasedKeys(); + private: + inline static std::map s_KeyData; + inline static std::map s_MouseData; + inline static std::map s_Controllers; }; } diff --git a/StarEngine/src/StarEngine/Core/KeyCodes.h b/StarEngine/src/StarEngine/Core/KeyCodes.h index b5154541..35a14110 100644 --- a/StarEngine/src/StarEngine/Core/KeyCodes.h +++ b/StarEngine/src/StarEngine/Core/KeyCodes.h @@ -1,144 +1,315 @@ #pragma once +#include +#include + namespace StarEngine { - using KeyCode = uint16_t; + typedef enum class KeyCode : uint16_t + { + // From glfw3.h + Space = 32, + Apostrophe = 39, /* ' */ + Comma = 44, /* , */ + Minus = 45, /* - */ + Period = 46, /* . */ + Slash = 47, /* / */ + + D0 = 48, /* 0 */ + D1 = 49, /* 1 */ + D2 = 50, /* 2 */ + D3 = 51, /* 3 */ + D4 = 52, /* 4 */ + D5 = 53, /* 5 */ + D6 = 54, /* 6 */ + D7 = 55, /* 7 */ + D8 = 56, /* 8 */ + D9 = 57, /* 9 */ + + Semicolon = 59, /* ; */ + Equal = 61, /* = */ + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + LeftBracket = 91, /* [ */ + Backslash = 92, /* \ */ + RightBracket = 93, /* ] */ + GraveAccent = 96, /* ` */ + + World1 = 161, /* non-US #1 */ + World2 = 162, /* non-US #2 */ - namespace Key + /* Function keys */ + Escape = 256, + Enter = 257, + Tab = 258, + Backspace = 259, + Insert = 260, + Delete = 261, + Right = 262, + Left = 263, + Down = 264, + Up = 265, + PageUp = 266, + PageDown = 267, + Home = 268, + End = 269, + CapsLock = 280, + ScrollLock = 281, + NumLock = 282, + PrintScreen = 283, + Pause = 284, + F1 = 290, + F2 = 291, + F3 = 292, + F4 = 293, + F5 = 294, + F6 = 295, + F7 = 296, + F8 = 297, + F9 = 298, + F10 = 299, + F11 = 300, + F12 = 301, + F13 = 302, + F14 = 303, + F15 = 304, + F16 = 305, + F17 = 306, + F18 = 307, + F19 = 308, + F20 = 309, + F21 = 310, + F22 = 311, + F23 = 312, + F24 = 313, + F25 = 314, + + /* Keypad */ + KP0 = 320, + KP1 = 321, + KP2 = 322, + KP3 = 323, + KP4 = 324, + KP5 = 325, + KP6 = 326, + KP7 = 327, + KP8 = 328, + KP9 = 329, + KPDecimal = 330, + KPDivide = 331, + KPMultiply = 332, + KPSubtract = 333, + KPAdd = 334, + KPEnter = 335, + KPEqual = 336, + + LeftShift = 340, + LeftControl = 341, + LeftAlt = 342, + LeftSuper = 343, + RightShift = 344, + RightControl = 345, + RightAlt = 346, + RightSuper = 347, + Menu = 348 + } Key; + + enum class KeyState { - enum : KeyCode - { - // From glfw3.h - Space = 32, - Apostrophe = 39, /* ' */ - Comma = 44, /* , */ - Minus = 45, /* - */ - Period = 46, /* . */ - Slash = 47, /* / */ - - D0 = 48, /* 0 */ - D1 = 49, /* 1 */ - D2 = 50, /* 2 */ - D3 = 51, /* 3 */ - D4 = 52, /* 4 */ - D5 = 53, /* 5 */ - D6 = 54, /* 6 */ - D7 = 55, /* 7 */ - D8 = 56, /* 8 */ - D9 = 57, /* 9 */ - - Semicolon = 59, /* ; */ - Equal = 61, /* = */ - - A = 65, - B = 66, - C = 67, - D = 68, - E = 69, - F = 70, - G = 71, - H = 72, - I = 73, - J = 74, - K = 75, - L = 76, - M = 77, - N = 78, - O = 79, - P = 80, - Q = 81, - R = 82, - S = 83, - T = 84, - U = 85, - V = 86, - W = 87, - X = 88, - Y = 89, - Z = 90, - - LeftBracket = 91, /* [ */ - Backslash = 92, /* \ */ - RightBracket = 93, /* ] */ - GraveAccent = 96, /* ` */ - - World1 = 161, /* non-US #1 */ - World2 = 162, /* non-US #2 */ - - /* Function keys */ - Escape = 256, - Enter = 257, - Tab = 258, - Backspace = 259, - Insert = 260, - Delete = 261, - Right = 262, - Left = 263, - Down = 264, - Up = 265, - PageUp = 266, - PageDown = 267, - Home = 268, - End = 269, - CapsLock = 280, - ScrollLock = 281, - NumLock = 282, - PrintScreen = 283, - Pause = 284, - F1 = 290, - F2 = 291, - F3 = 292, - F4 = 293, - F5 = 294, - F6 = 295, - F7 = 296, - F8 = 297, - F9 = 298, - F10 = 299, - F11 = 300, - F12 = 301, - F13 = 302, - F14 = 303, - F15 = 304, - F16 = 305, - F17 = 306, - F18 = 307, - F19 = 308, - F20 = 309, - F21 = 310, - F22 = 311, - F23 = 312, - F24 = 313, - F25 = 314, - - /* Keypad */ - KP0 = 320, - KP1 = 321, - KP2 = 322, - KP3 = 323, - KP4 = 324, - KP5 = 325, - KP6 = 326, - KP7 = 327, - KP8 = 328, - KP9 = 329, - KPDecimal = 330, - KPDivide = 331, - KPMultiply = 332, - KPSubtract = 333, - KPAdd = 334, - KPEnter = 335, - KPEqual = 336, - - LeftShift = 340, - LeftControl = 341, - LeftAlt = 342, - LeftSuper = 343, - RightShift = 344, - RightControl = 345, - RightAlt = 346, - RightSuper = 347, - Menu = 348 - }; + None = -1, + Pressed, + Held, + Released + }; + + enum class CursorMode + { + Normal = 0, + Hidden = 1, + Locked = 2 + }; + + typedef enum class MouseButton : uint16_t + { + Button0 = 0, + Button1 = 1, + Button2 = 2, + Button3 = 3, + Button4 = 4, + Button5 = 5, + Left = Button0, + Right = Button1, + Middle = Button2 + } Button; + + + inline std::ostream& operator<<(std::ostream& os, KeyCode keyCode) + { + os << static_cast(keyCode); + return os; + } + + inline std::ostream& operator<<(std::ostream& os, MouseButton button) + { + os << static_cast(button); + return os; } -} \ No newline at end of file +} + +// From glfw3.h +#define SE_KEY_SPACE ::StarEngine::Key::Space +#define SE_KEY_APOSTROPHE ::StarEngine::Key::Apostrophe /* ' */ +#define SE_KEY_COMMA ::StarEngine::Key::Comma /* , */ +#define SE_KEY_MINUS ::StarEngine::Key::Minus /* - */ +#define SE_KEY_PERIOD ::StarEngine::Key::Period /* . */ +#define SE_KEY_SLASH ::StarEngine::Key::Slash /* / */ +#define SE_KEY_0 ::StarEngine::Key::D0 +#define SE_KEY_1 ::StarEngine::Key::D1 +#define SE_KEY_2 ::StarEngine::Key::D2 +#define SE_KEY_3 ::StarEngine::Key::D3 +#define SE_KEY_4 ::StarEngine::Key::D4 +#define SE_KEY_5 ::StarEngine::Key::D5 +#define SE_KEY_6 ::StarEngine::Key::D6 +#define SE_KEY_7 ::StarEngine::Key::D7 +#define SE_KEY_8 ::StarEngine::Key::D8 +#define SE_KEY_9 ::StarEngine::Key::D9 +#define SE_KEY_SEMICOLON ::StarEngine::Key::Semicolon /* ; */ +#define SE_KEY_EQUAL ::StarEngine::Key::Equal /* = */ +#define SE_KEY_A ::StarEngine::Key::A +#define SE_KEY_B ::StarEngine::Key::B +#define SE_KEY_C ::StarEngine::Key::C +#define SE_KEY_D ::StarEngine::Key::D +#define SE_KEY_E ::StarEngine::Key::E +#define SE_KEY_F ::StarEngine::Key::F +#define SE_KEY_G ::StarEngine::Key::G +#define SE_KEY_H ::StarEngine::Key::H +#define SE_KEY_I ::StarEngine::Key::I +#define SE_KEY_J ::StarEngine::Key::J +#define SE_KEY_K ::StarEngine::Key::K +#define SE_KEY_L ::StarEngine::Key::L +#define SE_KEY_M ::StarEngine::Key::M +#define SE_KEY_N ::StarEngine::Key::N +#define SE_KEY_O ::StarEngine::Key::O +#define SE_KEY_P ::StarEngine::Key::P +#define SE_KEY_Q ::StarEngine::Key::Q +#define SE_KEY_R ::StarEngine::Key::R +#define SE_KEY_S ::StarEngine::Key::S +#define SE_KEY_T ::StarEngine::Key::T +#define SE_KEY_U ::StarEngine::Key::U +#define SE_KEY_V ::StarEngine::Key::V +#define SE_KEY_W ::StarEngine::Key::W +#define SE_KEY_X ::StarEngine::Key::X +#define SE_KEY_Y ::StarEngine::Key::Y +#define SE_KEY_Z ::StarEngine::Key::Z +#define SE_KEY_LEFT_BRACKET ::StarEngine::Key::LeftBracket /* [ */ +#define SE_KEY_BACKSLASH ::StarEngine::Key::Backslash /* \ */ +#define SE_KEY_RIGHT_BRACKET ::StarEngine::Key::RightBracket /* ] */ +#define SE_KEY_GRAVE_ACCENT ::StarEngine::Key::GraveAccent /* ` */ +#define SE_KEY_WORLD_1 ::StarEngine::Key::World1 /* non-US #1 */ +#define SE_KEY_WORLD_2 ::StarEngine::Key::World2 /* non-US #2 */ + +/* Function keys */ +#define SE_KEY_ESCAPE ::StarEngine::Key::Escape +#define SE_KEY_ENTER ::StarEngine::Key::Enter +#define SE_KEY_TAB ::StarEngine::Key::Tab +#define SE_KEY_BACKSPACE ::StarEngine::Key::Backspace +#define SE_KEY_INSERT ::StarEngine::Key::Insert +#define SE_KEY_DELETE ::StarEngine::Key::Delete +#define SE_KEY_RIGHT ::StarEngine::Key::Right +#define SE_KEY_LEFT ::StarEngine::Key::Left +#define SE_KEY_DOWN ::StarEngine::Key::Down +#define SE_KEY_UP ::StarEngine::Key::Up +#define SE_KEY_PAGE_UP ::StarEngine::Key::PageUp +#define SE_KEY_PAGE_DOWN ::StarEngine::Key::PageDown +#define SE_KEY_HOME ::StarEngine::Key::Home +#define SE_KEY_END ::StarEngine::Key::End +#define SE_KEY_CAPS_LOCK ::StarEngine::Key::CapsLock +#define SE_KEY_SCROLL_LOCK ::StarEngine::Key::ScrollLock +#define SE_KEY_NUM_LOCK ::StarEngine::Key::NumLock +#define SE_KEY_PRINT_SCREEN ::StarEngine::Key::PrintScreen +#define SE_KEY_PAUSE ::StarEngine::Key::Pause +#define SE_KEY_F1 ::StarEngine::Key::F1 +#define SE_KEY_F2 ::StarEngine::Key::F2 +#define SE_KEY_F3 ::StarEngine::Key::F3 +#define SE_KEY_F4 ::StarEngine::Key::F4 +#define SE_KEY_F5 ::StarEngine::Key::F5 +#define SE_KEY_F6 ::StarEngine::Key::F6 +#define SE_KEY_F7 ::StarEngine::Key::F7 +#define SE_KEY_F8 ::StarEngine::Key::F8 +#define SE_KEY_F9 ::StarEngine::Key::F9 +#define SE_KEY_F10 ::StarEngine::Key::F10 +#define SE_KEY_F11 ::StarEngine::Key::F11 +#define SE_KEY_F12 ::StarEngine::Key::F12 +#define SE_KEY_F13 ::StarEngine::Key::F13 +#define SE_KEY_F14 ::StarEngine::Key::F14 +#define SE_KEY_F15 ::StarEngine::Key::F15 +#define SE_KEY_F16 ::StarEngine::Key::F16 +#define SE_KEY_F17 ::StarEngine::Key::F17 +#define SE_KEY_F18 ::StarEngine::Key::F18 +#define SE_KEY_F19 ::StarEngine::Key::F19 +#define SE_KEY_F20 ::StarEngine::Key::F20 +#define SE_KEY_F21 ::StarEngine::Key::F21 +#define SE_KEY_F22 ::StarEngine::Key::F22 +#define SE_KEY_F23 ::StarEngine::Key::F23 +#define SE_KEY_F24 ::StarEngine::Key::F24 +#define SE_KEY_F25 ::StarEngine::Key::F25 + +/* Keypad */ +#define SE_KEY_KP_0 ::StarEngine::Key::KP0 +#define SE_KEY_KP_1 ::StarEngine::Key::KP1 +#define SE_KEY_KP_2 ::StarEngine::Key::KP2 +#define SE_KEY_KP_3 ::StarEngine::Key::KP3 +#define SE_KEY_KP_4 ::StarEngine::Key::KP4 +#define SE_KEY_KP_5 ::StarEngine::Key::KP5 +#define SE_KEY_KP_6 ::StarEngine::Key::KP6 +#define SE_KEY_KP_7 ::StarEngine::Key::KP7 +#define SE_KEY_KP_8 ::StarEngine::Key::KP8 +#define SE_KEY_KP_9 ::StarEngine::Key::KP9 +#define SE_KEY_KP_DECIMAL ::StarEngine::Key::KPDecimal +#define SE_KEY_KP_DIVIDE ::StarEngine::Key::KPDivide +#define SE_KEY_KP_MULTIPLY ::StarEngine::Key::KPMultiply +#define SE_KEY_KP_SUBTRACT ::StarEngine::Key::KPSubtract +#define SE_KEY_KP_ADD ::StarEngine::Key::KPAdd +#define SE_KEY_KP_ENTER ::StarEngine::Key::KPEnter +#define SE_KEY_KP_EQUAL ::StarEngine::Key::KPEqual + +#define SE_KEY_LEFT_SHIFT ::StarEngine::Key::LeftShift +#define SE_KEY_LEFT_CONTROL ::StarEngine::Key::LeftControl +#define SE_KEY_LEFT_ALT ::StarEngine::Key::LeftAlt +#define SE_KEY_LEFT_SUPER ::StarEngine::Key::LeftSuper +#define SE_KEY_RIGHT_SHIFT ::StarEngine::Key::RightShift +#define SE_KEY_RIGHT_CONTROL ::StarEngine::Key::RightControl +#define SE_KEY_RIGHT_ALT ::StarEngine::Key::RightAlt +#define SE_KEY_RIGHT_SUPER ::StarEngine::Key::RightSuper +#define SE_KEY_MENU ::StarEngine::Key::Menu + +// Mouse +#define SE_MOUSE_BUTTON_LEFT ::StarEngine::Button::Left +#define SE_MOUSE_BUTTON_RIGHT ::StarEngine::Button::Right +#define SE_MOUSE_BUTTON_MIDDLE ::StarEngine::Button::Middle diff --git a/StarEngine/src/StarEngine/Core/Layer.h b/StarEngine/src/StarEngine/Core/Layer.h index 540a32c9..3b9a03ce 100644 --- a/StarEngine/src/StarEngine/Core/Layer.h +++ b/StarEngine/src/StarEngine/Core/Layer.h @@ -1,8 +1,10 @@ #pragma once +#include "StarEngine/Core/Ref.h" #include "StarEngine/Core/Base.h" + #include "StarEngine/Core/Timestep.h" -#include "StarEngine/Events/Event.h" +#include "StarEngine/Core/Events/Event.h" namespace StarEngine { diff --git a/StarEngine/src/StarEngine/Core/LayerStack.cpp b/StarEngine/src/StarEngine/Core/LayerStack.cpp index 43ef8fba..b654eb90 100644 --- a/StarEngine/src/StarEngine/Core/LayerStack.cpp +++ b/StarEngine/src/StarEngine/Core/LayerStack.cpp @@ -1,12 +1,14 @@ #include "sepch.h" -#include "StarEngine/Core/LayerStack.h" +#include "LayerStack.h" namespace StarEngine { + LayerStack::LayerStack() + { + } + LayerStack::~LayerStack() { - for (Layer* layer : m_Layers) - delete layer; } void LayerStack::PushLayer(Layer* layer) @@ -22,22 +24,20 @@ namespace StarEngine { void LayerStack::PopLayer(Layer* layer) { - auto it = std::find(m_Layers.begin(), m_Layers.begin() + m_LayerInsertIndex, layer); - if (it != m_Layers.begin() + m_LayerInsertIndex) + auto it = std::find(m_Layers.begin(), m_Layers.end(), layer); + if (it != m_Layers.end()) { - layer->OnDetach(); m_Layers.erase(it); m_LayerInsertIndex--; } + } void LayerStack::PopOverlay(Layer* overlay) { - auto it = std::find(m_Layers.begin() + m_LayerInsertIndex, m_Layers.end(), overlay); - if (it != m_Layers.end()) { - overlay->OnDetach(); + auto it = std::find(m_Layers.begin(), m_Layers.end(), overlay); + if (it != m_Layers.end()) m_Layers.erase(it); - } } -} \ No newline at end of file +} diff --git a/StarEngine/src/StarEngine/Core/LayerStack.h b/StarEngine/src/StarEngine/Core/LayerStack.h index 1248dce6..74fb5068 100644 --- a/StarEngine/src/StarEngine/Core/LayerStack.h +++ b/StarEngine/src/StarEngine/Core/LayerStack.h @@ -1,15 +1,16 @@ #pragma once -#include "StarEngine/Core/Base.h" -#include "StarEngine/Core/Layer.h" +#include "Assert.h" +#include "Layer.h" #include namespace StarEngine { + class LayerStack { public: - LayerStack() = default; + LayerStack(); ~LayerStack(); void PushLayer(Layer* layer); @@ -17,18 +18,25 @@ namespace StarEngine { void PopLayer(Layer* layer); void PopOverlay(Layer* overlay); + Layer* operator[](size_t index) + { + SE_CORE_ASSERT(index >= 0 && index < m_Layers.size()); + return m_Layers[index]; + } + + const Layer* operator[](size_t index) const + { + SE_CORE_ASSERT(index >= 0 && index < m_Layers.size()); + return m_Layers[index]; + } + + size_t Size() const { return m_Layers.size(); } + std::vector::iterator begin() { return m_Layers.begin(); } std::vector::iterator end() { return m_Layers.end(); } - std::vector::reverse_iterator rbegin() { return m_Layers.rbegin(); } - std::vector::reverse_iterator rend() { return m_Layers.rend(); } - - std::vector::const_iterator begin() const { return m_Layers.begin(); } - std::vector::const_iterator end() const { return m_Layers.end(); } - std::vector::const_reverse_iterator rbegin() const { return m_Layers.rbegin(); } - std::vector::const_reverse_iterator rend() const { return m_Layers.rend(); } private: std::vector m_Layers; unsigned int m_LayerInsertIndex = 0; }; -} +} diff --git a/StarEngine/src/StarEngine/Core/Log.cpp b/StarEngine/src/StarEngine/Core/Log.cpp index 942387c4..f27bde79 100644 --- a/StarEngine/src/StarEngine/Core/Log.cpp +++ b/StarEngine/src/StarEngine/Core/Log.cpp @@ -1,32 +1,106 @@ #include "sepch.h" +#include "Log.h" -#include "StarEngine/Core/Log.h" +#include "spdlog/sinks/stdout_color_sinks.h" +#include "spdlog/sinks/basic_file_sink.h" +//#include "StarEngine/Editor/EditorConsole/EditorConsoleSink.h" -#include -#include +#include -namespace StarEngine -{ - Ref Log::s_CoreLogger; - Ref Log::s_ClientLogger; +#define SE_HAS_CONSOLE !SE_DIST + +namespace StarEngine { + + std::shared_ptr Log::s_CoreLogger; + std::shared_ptr Log::s_ClientLogger; + std::shared_ptr Log::s_EditorConsoleLogger; + + std::map Log::s_DefaultTagDetails = { + { "Animation", TagDetails{ true, Level::Warn } }, + { "Asset Pack", TagDetails{ true, Level::Warn } }, + { "AssetManager", TagDetails{ true, Level::Info } }, + { "AssetSystem", TagDetails{ true, Level::Info } }, + { "Assimp", TagDetails{ true, Level::Error } }, + { "Audio", TagDetails{ true, Level::Error } }, + { "Core", TagDetails{ true, Level::Trace } }, + { "GLFW", TagDetails{ true, Level::Error } }, + { "Memory", TagDetails{ true, Level::Error } }, + { "Mesh", TagDetails{ true, Level::Warn } }, + { "Physics", TagDetails{ true, Level::Warn } }, + { "Project", TagDetails{ true, Level::Warn } }, + { "Renderer", TagDetails{ true, Level::Info } }, + { "Scene", TagDetails{ true, Level::Info } }, + { "Scripting", TagDetails{ true, Level::Warn } }, + { "Sound Spatializer", TagDetails{ true, Level::Warn } }, + { "Timer", TagDetails{ false, Level::Trace } }, + { "miniaudio", TagDetails{ true, Level::Error } }, + }; void Log::Init() { - std::vector logSinks; - logSinks.emplace_back(std::make_shared()); - logSinks.emplace_back(std::make_shared("StarEngine.log", true)); + // Create "logs" directory if doesn't exist + std::string logsDirectory = "logs"; + if (!std::filesystem::exists(logsDirectory)) + std::filesystem::create_directories(logsDirectory); + + std::vector starSinks = + { + std::make_shared("logs/STARENGINE.log", true), +#if SE_HAS_CONSOLE + std::make_shared() +#endif + }; - logSinks[0]->set_pattern("%^[%T] %n: %v%$"); - logSinks[1]->set_pattern("[%T] [%l] %n: %v"); + std::vector appSinks = + { + std::make_shared("logs/APP.log", true), +#if SE_HAS_CONSOLE + std::make_shared() +#endif + }; - s_CoreLogger = std::make_shared("STARENGINE", begin(logSinks), end(logSinks)); - spdlog::register_logger(s_CoreLogger); + std::vector editorConsoleSinks = + { + std::make_shared("logs/APP.log", true), +#if SE_HAS_CONSOLE + //std::make_shared(1), + std::make_shared() +#endif + }; + + starSinks[0]->set_pattern("[%T] [%l] %n: %v"); + appSinks[0]->set_pattern("[%T] [%l] %n: %v"); + +#if SE_HAS_CONSOLE + starSinks[1]->set_pattern("%^[%T] %n: %v%$"); + appSinks[1]->set_pattern("%^[%T] %n: %v%$"); + for (auto sink : editorConsoleSinks) + sink->set_pattern("%^%v%$"); +#endif + + s_CoreLogger = std::make_shared("STARENGINE", starSinks.begin(), starSinks.end()); s_CoreLogger->set_level(spdlog::level::trace); - s_CoreLogger->flush_on(spdlog::level::trace); - s_ClientLogger = std::make_shared("APP", begin(logSinks), end(logSinks)); - spdlog::register_logger(s_ClientLogger); + s_ClientLogger = std::make_shared("APP", appSinks.begin(), appSinks.end()); s_ClientLogger->set_level(spdlog::level::trace); - s_ClientLogger->flush_on(spdlog::level::trace); + + s_EditorConsoleLogger = std::make_shared("Console", editorConsoleSinks.begin(), editorConsoleSinks.end()); + s_EditorConsoleLogger->set_level(spdlog::level::trace); + + SetDefaultTagSettings(); + } + + void Log::Shutdown() + { + s_EditorConsoleLogger.reset(); + s_ClientLogger.reset(); + s_CoreLogger.reset(); + spdlog::drop_all(); } + + void Log::SetDefaultTagSettings() + { + s_EnabledTags = s_DefaultTagDetails; + } + } diff --git a/StarEngine/src/StarEngine/Core/Log.h b/StarEngine/src/StarEngine/Core/Log.h index 7ae7c29b..5cdaf993 100644 --- a/StarEngine/src/StarEngine/Core/Log.h +++ b/StarEngine/src/StarEngine/Core/Log.h @@ -1,55 +1,255 @@ #pragma once #include "StarEngine/Core/Base.h" +#include "StarEngine/Core/LogCustomFormatters.h" -#define GLM_ENABLE_EXPERIMENTAL -#include "glm/gtx/string_cast.hpp" +#include -#pragma warning(push, 0) -#include "spdlog/spdlog.h" -#include "spdlog/fmt/ostr.h" -#pragma warning(pop) +#include +#include +#include +#include +#include namespace StarEngine { + class Log { + public: + enum class Type : uint8_t + { + Core = 0, Client = 1 + }; + enum class Level : uint8_t + { + Trace = 0, Info, Warn, Error, Fatal + }; + struct TagDetails + { + bool Enabled = true; + Level LevelFilter = Level::Trace; + }; + public: static void Init(); + static void Shutdown(); + + inline static std::shared_ptr& GetCoreLogger() { return s_CoreLogger; } + inline static std::shared_ptr& GetClientLogger() { return s_ClientLogger; } + inline static std::shared_ptr& GetEditorConsoleLogger() { return s_EditorConsoleLogger; } + + static bool HasTag(const std::string& tag) { return s_EnabledTags.find(tag) != s_EnabledTags.end(); } + static std::map& EnabledTags() { return s_EnabledTags; } + static void SetDefaultTagSettings(); + +#if defined(SE_PLATFORM_WINDOWS) + template + static void PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args); +#else + template + static void PrintMessage(Log::Type type, Log::Level level, const std::string_view format, Args&&... args); +#endif + + template + static void PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, std::format_string format, Args&&... args); + + static void PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, std::string_view message); + + template + static void PrintAssertMessage(Log::Type type, std::string_view prefix, std::format_string message, Args&&... args); + + static void PrintAssertMessage(Log::Type type, std::string_view prefix); + + public: + // Enum utils + static const char* LevelToString(Level level) + { + switch (level) + { + case Level::Trace: return "Trace"; + case Level::Info: return "Info"; + case Level::Warn: return "Warn"; + case Level::Error: return "Error"; + case Level::Fatal: return "Fatal"; + } + return ""; + } + static Level LevelFromString(std::string_view string) + { + if (string == "Trace") return Level::Trace; + if (string == "Info") return Level::Info; + if (string == "Warn") return Level::Warn; + if (string == "Error") return Level::Error; + if (string == "Fatal") return Level::Fatal; + + return Level::Trace; + } - static Ref& GetCoreLogger() { return s_CoreLogger; } - static Ref& GetClientLogger() { return s_ClientLogger; } private: - static Ref s_CoreLogger; - static Ref s_ClientLogger; + static std::shared_ptr s_CoreLogger; + static std::shared_ptr s_ClientLogger; + static std::shared_ptr s_EditorConsoleLogger; + + inline static std::map s_EnabledTags; + static std::map s_DefaultTagDetails; }; -} -template -inline OStream& operator<<(OStream& os, const glm::vec& vector) -{ - return os << glm::to_string(vector); } -template -inline OStream& operator<<(OStream& os, const glm::mat& matrix) -{ - return os << glm::to_string(matrix); -} +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Tagged logs (prefer these!) // +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -template -inline OStream& operator<<(OStream& os, glm::qua quaternion) -{ - return os << glm::to_string(quaternion); -} +// Core logging +#define SE_CORE_TRACE_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Trace, tag, __VA_ARGS__) +#define SE_CORE_INFO_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Info, tag, __VA_ARGS__) +#define SE_CORE_WARN_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Warn, tag, __VA_ARGS__) +#define SE_CORE_ERROR_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Error, tag, __VA_ARGS__) +#define SE_CORE_FATAL_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Fatal, tag, __VA_ARGS__) + +// Client logging +#define SE_TRACE_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Trace, tag, __VA_ARGS__) +#define SE_INFO_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Info, tag, __VA_ARGS__) +#define SE_WARN_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Warn, tag, __VA_ARGS__) +#define SE_ERROR_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Error, tag, __VA_ARGS__) +#define SE_FATAL_TAG(tag, ...) ::StarEngine::Log::PrintMessageTag(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Fatal, tag, __VA_ARGS__) + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Core Logging +#define SE_CORE_TRACE(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Trace, __VA_ARGS__) +#define SE_CORE_INFO(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Info, __VA_ARGS__) +#define SE_CORE_WARN(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Warn, __VA_ARGS__) +#define SE_CORE_ERROR(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Error, __VA_ARGS__) +#define SE_CORE_FATAL(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Core, ::StarEngine::Log::Level::Fatal, __VA_ARGS__) -#define SE_CORE_TRACE(...) ::StarEngine::Log::GetCoreLogger()->trace(__VA_ARGS__) -#define SE_CORE_INFO(...) ::StarEngine::Log::GetCoreLogger()->info(__VA_ARGS__) -#define SE_CORE_WARN(...) ::StarEngine::Log::GetCoreLogger()->warn(__VA_ARGS__) -#define SE_CORE_ERROR(...) ::StarEngine::Log::GetCoreLogger()->error(__VA_ARGS__) -#define SE_CORE_CRITICAL(...) ::StarEngine::Log::GetClientLogger()->critical(__VA_ARGS__) - -#define SE_TRACE(...) ::StarEngine::Log::GetClientLogger()->trace(__VA_ARGS__) -#define SE_INFO(...) ::StarEngine::Log::GetClientLogger()->info(__VA_ARGS__) -#define SE_WARN(...) ::StarEngine::Log::GetClientLogger()->warn(__VA_ARGS__) -#define SE_ERROR(...) ::StarEngine::Log::GetClientLogger()->error(__VA_ARGS__) -#define SE_CRITICAL(...) ::StarEngine::Log::GetClientLogger()->critical(__VA_ARGS__) +// Client Logging +#define SE_TRACE(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Trace, __VA_ARGS__) +#define SE_INFO(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Info, __VA_ARGS__) +#define SE_WARN(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Warn, __VA_ARGS__) +#define SE_ERROR(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Error, __VA_ARGS__) +#define SE_FATAL(...) ::StarEngine::Log::PrintMessage(::StarEngine::Log::Type::Client, ::StarEngine::Log::Level::Fatal, __VA_ARGS__) + +// Editor Console Logging Macros +#define SE_CONSOLE_LOG_TRACE(...) StarEngine::Log::GetEditorConsoleLogger()->trace(__VA_ARGS__) +#define SE_CONSOLE_LOG_INFO(...) StarEngine::Log::GetEditorConsoleLogger()->info(__VA_ARGS__) +#define SE_CONSOLE_LOG_WARN(...) StarEngine::Log::GetEditorConsoleLogger()->warn(__VA_ARGS__) +#define SE_CONSOLE_LOG_ERROR(...) StarEngine::Log::GetEditorConsoleLogger()->error(__VA_ARGS__) +#define SE_CONSOLE_LOG_FATAL(...) StarEngine::Log::GetEditorConsoleLogger()->critical(__VA_ARGS__) + +namespace StarEngine { + +#if defined(SE_PLATFORM_WINDOWS) + template + void Log::PrintMessage(Log::Type type, Log::Level level, std::format_string format, Args&&... args) +#else + template + void Log::PrintMessage(Log::Type type, Log::Level level, const std::string_view format, Args&&... args) +#endif + { + auto detail = s_EnabledTags[""]; + if (detail.Enabled && detail.LevelFilter <= level) + { + auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); + switch (level) + { + case Level::Trace: + logger->trace(format, std::forward(args)...); + break; + case Level::Info: + logger->info(format, std::forward(args)...); + break; + case Level::Warn: + logger->warn(format, std::forward(args)...); + break; + case Level::Error: + logger->error(format, std::forward(args)...); + break; + case Level::Fatal: + logger->critical(format, std::forward(args)...); + break; + } + } + } + + + template + void Log::PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, const std::format_string format, Args&&... args) + { + auto detail = s_EnabledTags[std::string(tag)]; + if (detail.Enabled && detail.LevelFilter <= level) + { + auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); + std::string formatted = std::format(format, std::forward(args)...); + switch (level) + { + case Level::Trace: + logger->trace("[{0}] {1}", tag, formatted); + break; + case Level::Info: + logger->info("[{0}] {1}", tag, formatted); + break; + case Level::Warn: + logger->warn("[{0}] {1}", tag, formatted); + break; + case Level::Error: + logger->error("[{0}] {1}", tag, formatted); + break; + case Level::Fatal: + logger->critical("[{0}] {1}", tag, formatted); + break; + } + } + } + + + inline void Log::PrintMessageTag(Log::Type type, Log::Level level, std::string_view tag, std::string_view message) + { + auto detail = s_EnabledTags[std::string(tag)]; + if (detail.Enabled && detail.LevelFilter <= level) + { + auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); + switch (level) + { + case Level::Trace: + logger->trace("[{0}] {1}", tag, message); + break; + case Level::Info: + logger->info("[{0}] {1}", tag, message); + break; + case Level::Warn: + logger->warn("[{0}] {1}", tag, message); + break; + case Level::Error: + logger->error("[{0}] {1}", tag, message); + break; + case Level::Fatal: + logger->critical("[{0}] {1}", tag, message); + break; + } + } + } + + + template + void Log::PrintAssertMessage(Log::Type type, std::string_view prefix, std::format_string message, Args&&... args) + { + auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); + auto formatted = std::format(message, std::forward(args)...); + logger->error("{0}: {1}", prefix, formatted); + +#if SE_ASSERT_MESSAGE_BOX + MessageBoxA(nullptr, formatted.data(), "StarEngine Assert", MB_OK | MB_ICONERROR); +#endif + } + + + inline void Log::PrintAssertMessage(Log::Type type, std::string_view prefix) + { + auto logger = (type == Type::Core) ? GetCoreLogger() : GetClientLogger(); + logger->error("{0}", prefix); +#if SE_ASSERT_MESSAGE_BOX + MessageBoxA(nullptr, "No message :(", "StarEngine Assert", MB_OK | MB_ICONERROR); +#endif + } +} diff --git a/StarEngine/src/StarEngine/Core/LogCustomFormatters.h b/StarEngine/src/StarEngine/Core/LogCustomFormatters.h new file mode 100644 index 00000000..54b84972 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/LogCustomFormatters.h @@ -0,0 +1,106 @@ +#pragma once + +#include "UUID.h" + +#include "glm/glm.hpp" + +#include +#include + +namespace std { + + template <> + struct formatter : formatter + { + template + FormatContext::iterator format(const StarEngine::UUID id, FormatContext& ctx) const + { + return formatter::format(static_cast(id), ctx); + } + }; + + + template <> + struct formatter : formatter + { + template + FormatContext::iterator format(const filesystem::path& path, FormatContext& ctx) const + { + return formatter::format(path.string(), ctx); + } + }; + + + template<> + struct formatter + { + char presentation = 'f'; + + constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) + { + auto it = ctx.begin(), end = ctx.end(); + if (it != end && (*it == 'f' || *it == 'e')) presentation = *it++; + + if (it != end && *it != '}') throw format_error("invalid format"); + + return it; + } + + template + auto format(const glm::vec2& vec, FormatContext& ctx) const -> decltype(ctx.out()) + { + return presentation == 'f' + ? format_to(ctx.out(), "({:.3f}, {:.3f})", vec.x, vec.y) + : format_to(ctx.out(), "({:.3e}, {:.3e})", vec.x, vec.y); + } + }; + + template<> + struct formatter + { + char presentation = 'f'; + + constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) + { + auto it = ctx.begin(), end = ctx.end(); + if (it != end && (*it == 'f' || *it == 'e')) presentation = *it++; + + if (it != end && *it != '}') throw format_error("invalid format"); + + return it; + } + + template + auto format(const glm::vec3& vec, FormatContext& ctx) const -> decltype(ctx.out()) + { + return presentation == 'f' + ? format_to(ctx.out(), "({:.3f}, {:.3f}, {:.3f})", vec.x, vec.y, vec.z) + : format_to(ctx.out(), "({:.3e}, {:.3e}, {:.3e})", vec.x, vec.y, vec.z); + } + }; + + template<> + struct formatter + { + char presentation = 'f'; + + constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) + { + auto it = ctx.begin(), end = ctx.end(); + if (it != end && (*it == 'f' || *it == 'e')) presentation = *it++; + + if (it != end && *it != '}') throw format_error("invalid format"); + + return it; + } + + template + auto format(const glm::vec4& vec, FormatContext& ctx) const -> decltype(ctx.out()) + { + return presentation == 'f' + ? format_to(ctx.out(), "({:.3f}, {:.3f}, {:.3f}, {:.3f})", vec.x, vec.y, vec.z, vec.w) + : format_to(ctx.out(), "({:.3e}, {:.3e}, {:.3e}, {:.3e})", vec.x, vec.y, vec.z, vec.w); + } + }; + +} diff --git a/StarEngine/src/StarEngine/Core/Math/AABB.h b/StarEngine/src/StarEngine/Core/Math/AABB.h new file mode 100644 index 00000000..3eb98ee4 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Math/AABB.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace StarEngine { + + struct AABB + { + glm::vec3 Min, Max; + + AABB() + : Min(0.0f), Max(0.0f) {} + + AABB(const glm::vec3& min, const glm::vec3& max) + : Min(min), Max(max) {} + + glm::vec3 Size() { return Max - Min; } + glm::vec3 Center() { return Min + Size() * 0.5f; } + + }; + + +} diff --git a/StarEngine/src/StarEngine/Core/Math/Mat4.cpp b/StarEngine/src/StarEngine/Core/Math/Mat4.cpp new file mode 100644 index 00000000..ea34eaab --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Math/Mat4.cpp @@ -0,0 +1,19 @@ +#include "sepch.h" +#include "Mat4.h" + +#include + +namespace StarEngine { + + /*glm::mat4 Mat4::FromAssimpMat4(const aiMatrix4x4& matrix) + { + glm::mat4 result; + //the a,b,c,d in assimp is the row ; the 1,2,3,4 is the column + result[0][0] = matrix.a1; result[1][0] = matrix.a2; result[2][0] = matrix.a3; result[3][0] = matrix.a4; + result[0][1] = matrix.b1; result[1][1] = matrix.b2; result[2][1] = matrix.b3; result[3][1] = matrix.b4; + result[0][2] = matrix.c1; result[1][2] = matrix.c2; result[2][2] = matrix.c3; result[3][2] = matrix.c4; + result[0][3] = matrix.d1; result[1][3] = matrix.d2; result[2][3] = matrix.d3; result[3][3] = matrix.d4; + return result; + }*/ + +} diff --git a/StarEngine/src/StarEngine/Core/Math/Mat4.h b/StarEngine/src/StarEngine/Core/Math/Mat4.h new file mode 100644 index 00000000..9d27f880 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Math/Mat4.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace StarEngine { + + class Mat4 + { + public: + // static glm::mat4 FromAssimpMat4(const aiMatrix4x4& matrix); + }; + +} diff --git a/StarEngine/src/StarEngine/Core/Math/Noise.cpp b/StarEngine/src/StarEngine/Core/Math/Noise.cpp new file mode 100644 index 00000000..e1af48ef --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Math/Noise.cpp @@ -0,0 +1,100 @@ +#include "sepch.h" +#include "Noise.h" + +#include "FastNoise/FastNoise.h" + +namespace StarEngine { + + static FastNoise s_FastNoise; + static std::uniform_real_distribution s_Jitters(0.f, 1.f); + static std::default_random_engine s_JitterGenerator(1337u); + + Noise::Noise(int seed) + { + m_FastNoise = snew FastNoise(seed); + m_FastNoise->SetNoiseType(FastNoise::Simplex); + } + + Noise::~Noise() + { + sdelete m_FastNoise; + } + + float Noise::GetFrequency() const + { + return m_FastNoise->GetFrequency(); + } + + void Noise::SetFrequency(float frequency) + { + m_FastNoise->SetFrequency(frequency); + } + + int Noise::GetFractalOctaves() const + { + return m_FastNoise->GetFractalOctaves(); + } + + void Noise::SetFractalOctaves(int octaves) + { + m_FastNoise->SetFractalOctaves(octaves); + } + + float Noise::GetFractalLacunarity() const + { + return m_FastNoise->GetFractalLacunarity(); + } + + void Noise::SetFractalLacunarity(float lacunarity) + { + return m_FastNoise->SetFractalLacunarity(lacunarity); + } + + float Noise::GetFractalGain() const + { + return m_FastNoise->GetFractalGain(); + } + + void Noise::SetFractalGain(float gain) + { + m_FastNoise->SetFractalGain(gain); + } + + float Noise::Get(float x, float y) + { + return m_FastNoise->GetNoise(x, y); + } + + void Noise::SetSeed(int seed) + { + s_FastNoise.SetSeed(seed); + } + + float Noise::PerlinNoise(float x, float y) + { + s_FastNoise.SetNoiseType(FastNoise::Perlin); + float result = s_FastNoise.GetNoise(x, y); // This returns a value between -1 and 1 + return result; + } + + std::array Noise::HBAOJitter() + { + constexpr float PI = 3.14159265358979323846264338f; + const float numDir = 8.f; // keep in sync to glsl + + std::array result {}; + + for (int i = 0; i < 16; i++) + { + float Rand1 = s_Jitters(s_JitterGenerator); + float Rand2 = s_Jitters(s_JitterGenerator); + // Use random rotation angles in [0,2PI/NUM_DIRECTIONS) + const float Angle = 2.f * PI * Rand1 / numDir; + result[i].x = cosf(Angle); + result[i].y = sinf(Angle); + result[i].z = Rand2; + result[i].w = 0; + } + return result; + } +} diff --git a/StarEngine/src/StarEngine/Core/Math/Noise.h b/StarEngine/src/StarEngine/Core/Math/Noise.h new file mode 100644 index 00000000..bd770be7 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Math/Noise.h @@ -0,0 +1,35 @@ +#pragma once + +class FastNoise; + +namespace StarEngine { + + class Noise + { + public: + Noise(int seed = 8); + ~Noise(); + + float GetFrequency() const; + void SetFrequency(float frequency); + + int GetFractalOctaves() const; + void SetFractalOctaves(int octaves); + + float GetFractalLacunarity() const; + void SetFractalLacunarity(float lacunarity); + + float GetFractalGain() const; + void SetFractalGain(float gain); + + float Get(float x, float y); + + // Static API + static void SetSeed(int seed); + static float PerlinNoise(float x, float y); + static std::array HBAOJitter(); + private: + FastNoise* m_FastNoise = nullptr; + }; + +} diff --git a/StarEngine/src/StarEngine/Core/Math/Ray.h b/StarEngine/src/StarEngine/Core/Math/Ray.h new file mode 100644 index 00000000..158db35b --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Math/Ray.h @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include "AABB.h" + +namespace StarEngine { + + struct Ray + { + glm::vec3 Origin, Direction; + + Ray(const glm::vec3& origin, const glm::vec3& direction) + { + Origin = origin; + Direction = direction; + } + + static Ray Zero() + { + return { {0.0f, 0.0f, 0.0f},{0.0f, 0.0f, 0.0f} }; + } + + bool IntersectsAABB(const AABB& aabb, float& t) const + { + glm::vec3 dirfrac; + // r.dir is unit direction vector of ray + dirfrac.x = 1.0f / Direction.x; + dirfrac.y = 1.0f / Direction.y; + dirfrac.z = 1.0f / Direction.z; + // lb is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner + // r.org is origin of ray + const glm::vec3& lb = aabb.Min; + const glm::vec3& rt = aabb.Max; + float t1 = (lb.x - Origin.x) * dirfrac.x; + float t2 = (rt.x - Origin.x) * dirfrac.x; + float t3 = (lb.y - Origin.y) * dirfrac.y; + float t4 = (rt.y - Origin.y) * dirfrac.y; + float t5 = (lb.z - Origin.z) * dirfrac.z; + float t6 = (rt.z - Origin.z) * dirfrac.z; + + float tmin = glm::max(glm::max(glm::min(t1, t2), glm::min(t3, t4)), glm::min(t5, t6)); + float tmax = glm::min(glm::min(glm::max(t1, t2), glm::max(t3, t4)), glm::max(t5, t6)); + + // if tmax < 0, ray (line) is intersecting AABB, but the whole AABB is behind us + if (tmax < 0) + { + t = tmax; + return false; + } + + // if tmin > tmax, ray doesn't intersect AABB + if (tmin > tmax) + { + t = tmax; + return false; + } + + t = tmin; + return true; + } + + bool IntersectsTriangle(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c, float& t) const + { + const glm::vec3 E1 = b - a; + const glm::vec3 E2 = c - a; + const glm::vec3 N = cross(E1, E2); + const float det = -glm::dot(Direction, N); + const float invdet = 1.f / det; + const glm::vec3 AO = Origin - a; + const glm::vec3 DAO = glm::cross(AO, Direction); + const float u = glm::dot(E2, DAO) * invdet; + const float v = -glm::dot(E1, DAO) * invdet; + t = glm::dot(AO, N) * invdet; + return (det >= 1e-6f && t >= 0.0f && u >= 0.0f && v >= 0.0f && (u + v) <= 1.0f); + } + + }; + +} diff --git a/StarEngine/src/StarEngine/Core/Memory.cpp b/StarEngine/src/StarEngine/Core/Memory.cpp new file mode 100644 index 00000000..fa93143b --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Memory.cpp @@ -0,0 +1,259 @@ +#include "sepch.h" +#include "Memory.h" + +#include "Log.h" +#include "StarEngine/Debug/Profiler.h" + +#include + +namespace StarEngine { + + static StarEngine::AllocationStats s_GlobalStats; + + static bool s_InInit = false; + + void Allocator::Init() + { + if (s_Data) + return; + + s_InInit = true; + AllocatorData* data = (AllocatorData*)Allocator::AllocateRaw(sizeof(AllocatorData)); + new(data) AllocatorData(); + s_Data = data; + s_InInit = false; + } + + void* Allocator::AllocateRaw(size_t size) + { + return malloc(size); + } + + void* Allocator::Allocate(size_t size) + { + if (s_InInit) + return AllocateRaw(size); + + if (!s_Data) + Init(); + + void* memory = malloc(size); + + { + std::scoped_lock lock(s_Data->m_Mutex); + Allocation& alloc = s_Data->m_AllocationMap[memory]; + alloc.Memory = memory; + alloc.Size = size; + + s_GlobalStats.TotalAllocated += size; + } + +#if SE_ENABLE_PROFILING + TracyAlloc(memory, size); +#endif + + return memory; + } + + void* Allocator::Allocate(size_t size, const char* desc) + { + if (!s_Data) + Init(); + + void* memory = malloc(size); + + { + std::scoped_lock lock(s_Data->m_Mutex); + Allocation& alloc = s_Data->m_AllocationMap[memory]; + alloc.Memory = memory; + alloc.Size = size; + alloc.Category = desc; + + s_GlobalStats.TotalAllocated += size; + if (desc) + s_Data->m_AllocationStatsMap[desc].TotalAllocated += size; + } + +#if SE_ENABLE_PROFILING + TracyAlloc(memory, size); +#endif + + return memory; + } + + void* Allocator::Allocate(size_t size, const char* file, int line) + { + if (!s_Data) + Init(); + + void* memory = malloc(size); + + { + std::scoped_lock lock(s_Data->m_Mutex); + Allocation& alloc = s_Data->m_AllocationMap[memory]; + alloc.Memory = memory; + alloc.Size = size; + alloc.Category = file; + + s_GlobalStats.TotalAllocated += size; + s_Data->m_AllocationStatsMap[file].TotalAllocated += size; + } + +#if SE_ENABLE_PROFILING + TracyAlloc(memory, size); +#endif + + return memory; + } + + void Allocator::Free(void* memory) + { + if (memory == nullptr) + return; + + { + bool found = false; + { + std::scoped_lock lock(s_Data->m_Mutex); + auto allocMapIt = s_Data->m_AllocationMap.find(memory); + found = allocMapIt != s_Data->m_AllocationMap.end(); + if (found) + { + const Allocation& alloc = allocMapIt->second; + s_GlobalStats.TotalFreed += alloc.Size; + if (alloc.Category) + s_Data->m_AllocationStatsMap[alloc.Category].TotalFreed += alloc.Size; + + s_Data->m_AllocationMap.erase(memory); + } + } + +#if SE_ENABLE_PROFILING + TracyFree(memory); +#endif + +#ifndef SE_DIST + if (!found) + SE_CORE_WARN_TAG("Memory", "Memory block {0} not present in alloc map", memory); +#endif + } + + free(memory); + } + + void Allocator::Free(void* memory, size_t size) + { + if (memory == nullptr) + return; + + { + bool found = false; + { + std::scoped_lock lock(s_Data->m_Mutex); + auto allocMapIt = s_Data->m_AllocationMap.find(memory); + found = allocMapIt != s_Data->m_AllocationMap.end(); + if (found) + { + const Allocation& alloc = allocMapIt->second; + SE_CORE_VERIFY(size == alloc.Size); + s_GlobalStats.TotalFreed += alloc.Size; + if (alloc.Category) + s_Data->m_AllocationStatsMap[alloc.Category].TotalFreed += alloc.Size; + + s_Data->m_AllocationMap.erase(memory); + } + } + +#if SE_ENABLE_PROFILING + TracyFree(memory); +#endif + +#ifndef SE_DIST + if (!found) + SE_CORE_WARN_TAG("Memory", "Memory block {0} not present in alloc map", memory); +#endif + } + + free(memory); + } + + namespace Memory { + + const AllocationStats& GetAllocationStats() { return s_GlobalStats; } + } +} + +#ifdef SE_TRACK_MEMORY && SE_PLATFORM_WINDOWS + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new(size_t size) +{ + return StarEngine::Allocator::Allocate(size); +} + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new[](size_t size) +{ + return StarEngine::Allocator::Allocate(size); +} + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new(size_t size, const char* desc) +{ + return StarEngine::Allocator::Allocate(size, desc); +} + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new[](size_t size, const char* desc) +{ + return StarEngine::Allocator::Allocate(size, desc); +} + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new(size_t size, const char* file, int line) +{ + return StarEngine::Allocator::Allocate(size, file, line); +} + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new[](size_t size, const char* file, int line) +{ + return StarEngine::Allocator::Allocate(size, file, line); +} + +void __CRTDECL operator delete(void* memory) +{ + return StarEngine::Allocator::Free(memory); +} + +void __CRTDECL operator delete(void* memory, size_t size) +{ + return StarEngine::Allocator::Free(memory, size); +} + +void __CRTDECL operator delete(void* memory, const char* desc) +{ + return StarEngine::Allocator::Free(memory); +} + +void __CRTDECL operator delete(void* memory, const char* file, int line) +{ + return StarEngine::Allocator::Free(memory); +} + +void __CRTDECL operator delete[](void* memory) +{ + return StarEngine::Allocator::Free(memory); +} + +void __CRTDECL operator delete[](void* memory, const char* desc) +{ + return StarEngine::Allocator::Free(memory); +} + +void __CRTDECL operator delete[](void* memory, const char* file, int line) +{ + return StarEngine::Allocator::Free(memory); +} + +#endif diff --git a/StarEngine/src/StarEngine/Core/Memory.h b/StarEngine/src/StarEngine/Core/Memory.h new file mode 100644 index 00000000..7d7fcfdf --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Memory.h @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace StarEngine { + + struct AllocationStats + { + size_t TotalAllocated = 0; + size_t TotalFreed = 0; + }; + + struct Allocation + { + void* Memory = 0; + size_t Size = 0; + const char* Category = 0; + }; + + namespace Memory + { + const AllocationStats& GetAllocationStats(); + } + + template + struct Mallocator + { + typedef T value_type; + + Mallocator() = default; + template constexpr Mallocator(const Mallocator &) noexcept {} + + T* allocate(std::size_t n) + { +#undef max + if (n > std::numeric_limits::max() / sizeof(T)) + throw std::bad_array_new_length(); + + if (auto p = static_cast(std::malloc(n * sizeof(T)))) { + return p; + } + + throw std::bad_alloc(); + } + + void deallocate(T* p, std::size_t n) noexcept { + std::free(p); + } + }; + + struct AllocatorData + { + using MapAlloc = Mallocator>; + using StatsMapAlloc = Mallocator>; + + using AllocationStatsMap = std::map, StatsMapAlloc>; + + std::map, MapAlloc> m_AllocationMap; + AllocationStatsMap m_AllocationStatsMap; + + std::mutex m_Mutex, m_StatsMutex; + }; + + + class Allocator + { + public: + static void Init(); + + static void* AllocateRaw(size_t size); + + static void* Allocate(size_t size); + static void* Allocate(size_t size, const char* desc); + static void* Allocate(size_t size, const char* file, int line); + static void Free(void* memory); + static void Free(void* memory, size_t size); + + static const AllocatorData::AllocationStatsMap& GetAllocationStats() { return s_Data->m_AllocationStatsMap; } + private: + inline static AllocatorData* s_Data = nullptr; + }; + +} + +#if SE_TRACK_MEMORY + +#ifdef SE_PLATFORM_WINDOWS + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new(size_t size); + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new[](size_t size); + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new(size_t size, const char* desc); + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new[](size_t size, const char* desc); + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new(size_t size, const char* file, int line); + +_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(size) _VCRT_ALLOCATOR +void* __CRTDECL operator new[](size_t size, const char* file, int line); + +void __CRTDECL operator delete(void* memory); +void __CRTDECL operator delete(void* memory, size_t size); +void __CRTDECL operator delete(void* memory, const char* desc); +void __CRTDECL operator delete(void* memory, const char* file, int line); +void __CRTDECL operator delete[](void* memory); +void __CRTDECL operator delete[](void* memory, const char* desc); +void __CRTDECL operator delete[](void* memory, const char* file, int line); + +#define snew new(__FILE__, __LINE__) +#define sdelete delete + +#else +#warning "Memory tracking not available on non-Windows platform" +#define snew new +#define sdelete delete + +#endif + +#else + +#define snew new +#define sdelete delete + +#endif diff --git a/StarEngine/src/StarEngine/Core/MouseCodes.h b/StarEngine/src/StarEngine/Core/MouseCodes.h deleted file mode 100644 index bca7cba1..00000000 --- a/StarEngine/src/StarEngine/Core/MouseCodes.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -namespace StarEngine -{ - using MouseCode = uint16_t; - - namespace Mouse - { - enum : MouseCode - { - // From glfw3.h - Button0 = 0, - Button1 = 1, - Button2 = 2, - Button3 = 3, - Button4 = 4, - Button5 = 5, - Button6 = 6, - Button7 = 7, - - ButtonLast = Button7, - ButtonLeft = Button0, - ButtonRight = Button1, - ButtonMiddle = Button2 - }; - } -} \ No newline at end of file diff --git a/StarEngine/src/StarEngine/Core/Platform.cpp b/StarEngine/src/StarEngine/Core/Platform.cpp new file mode 100644 index 00000000..075f61b3 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Platform.cpp @@ -0,0 +1,31 @@ +#include "sepch.h" +#include "Platform.h" + +#include +#include + +#include "spdlog/fmt/chrono.h" + +namespace StarEngine { + + uint64_t Platform::GetCurrentDateTimeU64() + { + std::string string = GetCurrentDateTimeString(); + return std::stoull(string); + } + + std::string Platform::GetCurrentDateTimeString() + { + std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm* localTime = std::localtime(¤tTime); + + int year = localTime->tm_year + 1900; + int month = localTime->tm_mon + 1; + int day = localTime->tm_mday + 1; + int hour = localTime->tm_hour; + int minute = localTime->tm_min; + + return std::format("{}{:02}{:02}{:02}{:02}", year, month, day, hour, minute); + //return std::format("{:%Y%m%d%H%M}", *localTime); + } +} diff --git a/StarEngine/src/StarEngine/Core/Platform.h b/StarEngine/src/StarEngine/Core/Platform.h new file mode 100644 index 00000000..5310d5e5 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Platform.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace StarEngine { + + class Platform + { + public: + static uint64_t GetCurrentDateTimeU64(); + static std::string GetCurrentDateTimeString(); + }; + +} diff --git a/StarEngine/src/StarEngine/Core/PlatformDetection.h b/StarEngine/src/StarEngine/Core/PlatformDetection.h deleted file mode 100644 index 9f52bb75..00000000 --- a/StarEngine/src/StarEngine/Core/PlatformDetection.h +++ /dev/null @@ -1,40 +0,0 @@ -// Platform detection using predefined macros -#ifdef _WIN32 - /* Windows x64/x86 */ -#ifdef _WIN64 - /* Windows x64 */ -#define SE_PLATFORM_WINDOWS -#else - /* Windows x86 */ -#error "x86 Builds are not supported!" -#endif -#elif defined(__APPLE__) || defined(__MACH__) -#include -/* TARGET_OS_MAC exists on all the platforms - * so we must check all of them (in this order) - * to ensure that we're running on MAC - * and not some other Apple platform */ -#if TARGET_IPHONE_SIMULATOR == 1 -#error "IOS simulator is not supported!" -#elif TARGET_OS_IPHONE == 1 -#define SE_PLATFORM_IOS -#error "IOS is not supported!" -#elif TARGET_OS_MAC == 1 -#define SE_PLATFORM_MACOS -#error "MacOS is not supported!" -#else -#error "Unknown Apple platform!" -#endif - /* We also have to check __ANDROID__ before __linux__ - * since android is based on the linux kernel - * it has __linux__ defined */ -#elif defined(__ANDROID__) -#define SE_PLATFORM_ANDROID -#error "Android is not supported!" -#elif defined(__linux__) -#define SE_PLATFORM_LINUX -#error "Linux is not supported!" -#else - /* Unknown compiler/platform */ -#error "Unknown platform!" -#endif // End of platform detection \ No newline at end of file diff --git a/StarEngine/src/StarEngine/Core/Ref.cpp b/StarEngine/src/StarEngine/Core/Ref.cpp new file mode 100644 index 00000000..7777d0f7 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Ref.cpp @@ -0,0 +1,35 @@ +#include "sepch.h" + +#include + +namespace StarEngine { + + static std::unordered_set s_LiveReferences; + static std::mutex s_LiveReferenceMutex; + + namespace RefUtils { + + void AddToLiveReferences(void* instance) + { + std::scoped_lock lock(s_LiveReferenceMutex); + SE_CORE_ASSERT(instance); + s_LiveReferences.insert(instance); + } + + void RemoveFromLiveReferences(void* instance) + { + std::scoped_lock lock(s_LiveReferenceMutex); + SE_CORE_ASSERT(instance); + SE_CORE_ASSERT(s_LiveReferences.find(instance) != s_LiveReferences.end()); + s_LiveReferences.erase(instance); + } + + bool IsLive(void* instance) + { + SE_CORE_ASSERT(instance); + return s_LiveReferences.find(instance) != s_LiveReferences.end(); + } + } + + +} diff --git a/StarEngine/src/StarEngine/Core/Ref.h b/StarEngine/src/StarEngine/Core/Ref.h new file mode 100644 index 00000000..ceadfe4c --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Ref.h @@ -0,0 +1,244 @@ +#pragma once + +#include "Memory.h" + +#include +#include +#include + +namespace StarEngine { + + class RefCounted + { + public: + virtual ~RefCounted() = default; + + void IncRefCount() const + { + ++m_RefCount; + } + void DecRefCount() const + { + --m_RefCount; + } + + uint32_t GetRefCount() const { return m_RefCount.load(); } + private: + mutable std::atomic m_RefCount = 0; + }; + + namespace RefUtils { + void AddToLiveReferences(void* instance); + void RemoveFromLiveReferences(void* instance); + bool IsLive(void* instance); + } + + template + class Ref + { + public: + Ref() + : m_Instance(nullptr) + { + } + + Ref(std::nullptr_t n) + : m_Instance(nullptr) + { + } + + Ref(T* instance) + : m_Instance(instance) + { + static_assert(std::is_base_of::value, "Class is not RefCounted!"); + + IncRef(); + } + + template + Ref(const Ref& other) + { + m_Instance = (T*)other.m_Instance; + IncRef(); + } + + template + Ref(Ref&& other) + { + m_Instance = (T*)other.m_Instance; + other.m_Instance = nullptr; + } + + static Ref CopyWithoutIncrement(const Ref& other) + { + Ref result = nullptr; + result->m_Instance = other.m_Instance; + return result; + } + + ~Ref() + { + DecRef(); + } + + Ref(const Ref& other) + : m_Instance(other.m_Instance) + { + IncRef(); + } + + Ref& operator=(std::nullptr_t) + { + DecRef(); + m_Instance = nullptr; + return *this; + } + + Ref& operator=(const Ref& other) + { + if (this == &other) + return *this; + + other.IncRef(); + DecRef(); + + m_Instance = other.m_Instance; + return *this; + } + + template + Ref& operator=(const Ref& other) + { + other.IncRef(); + DecRef(); + + m_Instance = other.m_Instance; + return *this; + } + + template + Ref& operator=(Ref&& other) + { + DecRef(); + + m_Instance = other.m_Instance; + other.m_Instance = nullptr; + return *this; + } + + operator bool() { return m_Instance != nullptr; } + operator bool() const { return m_Instance != nullptr; } + + T* operator->() { return m_Instance; } + const T* operator->() const { return m_Instance; } + + T& operator*() { return *m_Instance; } + const T& operator*() const { return *m_Instance; } + + T* Raw() { return m_Instance; } + const T* Raw() const { return m_Instance; } + + void Reset(T* instance = nullptr) + { + DecRef(); + m_Instance = instance; + } + + template + Ref As() const + { + return Ref(*this); + } + + template + static Ref Create(Args&&... args) + { +#if SE_TRACK_MEMORY && defined(SE_PLATFORM_WINDOWS) + return Ref(new(typeid(T).name()) T(std::forward(args)...)); +#else + return Ref(new T(std::forward(args)...)); +#endif + } + + bool operator==(const Ref& other) const + { + return m_Instance == other.m_Instance; + } + + bool operator!=(const Ref& other) const + { + return !(*this == other); + } + + bool EqualsObject(const Ref& other) + { + if (!m_Instance || !other.m_Instance) + return false; + + return *m_Instance == *other.m_Instance; + } + private: + void IncRef() const + { + if (m_Instance) + { + m_Instance->IncRefCount(); + RefUtils::AddToLiveReferences((void*)m_Instance); + } + } + + void DecRef() const + { + if (m_Instance) + { + m_Instance->DecRefCount(); + + if (m_Instance->GetRefCount() == 0) + { + delete m_Instance; + RefUtils::RemoveFromLiveReferences((void*)m_Instance); + m_Instance = nullptr; + } + } + } + + template + friend class Ref; + mutable T* m_Instance; + }; + + template + class WeakRef + { + public: + WeakRef() = default; + + WeakRef(Ref ref) + { + m_Instance = ref.Raw(); + } + + WeakRef(T* instance) + { + m_Instance = instance; + } + + T* operator->() { return m_Instance; } + const T* operator->() const { return m_Instance; } + + T& operator*() { return *m_Instance; } + const T& operator*() const { return *m_Instance; } + + bool IsValid() const { return m_Instance ? RefUtils::IsLive(m_Instance) : false; } + operator bool() const { return IsValid(); } + + template + WeakRef As() const + { + return WeakRef(dynamic_cast(m_Instance)); + } + private: + T* m_Instance = nullptr; + }; + +} diff --git a/StarEngine/src/StarEngine/Core/RenderThread.h b/StarEngine/src/StarEngine/Core/RenderThread.h new file mode 100644 index 00000000..bf5424db --- /dev/null +++ b/StarEngine/src/StarEngine/Core/RenderThread.h @@ -0,0 +1,56 @@ +#pragma once + +#include "Thread.h" + +#include + +namespace StarEngine { + + struct RenderThreadData; + + enum class ThreadingPolicy + { + // MultiThreaded will create a Render Thread + None = 0, SingleThreaded, MultiThreaded + }; + + class RenderThread + { + public: + enum class State + { + Idle = 0, + Busy, + Kick + }; + public: + RenderThread(ThreadingPolicy coreThreadingPolicy); + ~RenderThread(); + + void Run(); + bool IsRunning() const { return m_IsRunning; } + void Terminate(); + + void Wait(State waitForState); + void WaitAndSet(State waitForState, State setToState); + void Set(State setToState); + + void NextFrame(); + void BlockUntilRenderComplete(); + void Kick(); + + void Pump(); + + static bool IsCurrentThreadRT(); + private: + RenderThreadData* m_Data; + ThreadingPolicy m_ThreadingPolicy; + + Thread m_RenderThread; + + bool m_IsRunning = false; + + std::atomic m_AppThreadFrame = 0; + }; + +} diff --git a/StarEngine/src/StarEngine/Core/Thread.h b/StarEngine/src/StarEngine/Core/Thread.h new file mode 100644 index 00000000..3eacaac3 --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Thread.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +namespace StarEngine { + + class Thread + { + public: + Thread(const std::string& name); + + template + void Dispatch(Fn&& func, Args&&... args) + { + m_Thread = std::thread(func, std::forward(args)...); + SetName(m_Name); + } + + void SetName(const std::string& name); + + void Join(); + + std::thread::id GetID() const; + private: + std::string m_Name; + std::thread m_Thread; + }; + + class ThreadSignal + { + public: + ThreadSignal(const std::string& name, bool manualReset = false); + + void Wait(); + void Signal(); + void Reset(); + private: + void* m_SignalHandle = nullptr; + }; + +} diff --git a/StarEngine/src/StarEngine/Core/Timer.h b/StarEngine/src/StarEngine/Core/Timer.h index c2ade7c9..78fcd79b 100644 --- a/StarEngine/src/StarEngine/Core/Timer.h +++ b/StarEngine/src/StarEngine/Core/Timer.h @@ -1,34 +1,129 @@ #pragma once #include +#include +#include +#include + +#include "Log.h" namespace StarEngine { - class Timer - { + class Timer { public: - Timer() - { - Reset(); - } + Timer() { Reset(); } - void Timer::Reset() - { + SE_FORCE_INLINE void Reset() { m_Start = std::chrono::high_resolution_clock::now(); } - float Timer::Elapsed() + SE_FORCE_INLINE float Elapsed() const { + return std::chrono::duration(std::chrono::high_resolution_clock::now() - m_Start).count(); + } + + SE_FORCE_INLINE float ElapsedMillis() const { + return std::chrono::duration(std::chrono::high_resolution_clock::now() - m_Start).count(); + } + + private: + std::chrono::time_point m_Start; + }; + + class ScopedTimer { + public: + explicit ScopedTimer(std::string name) + : m_Name(std::move(name)) { + } + + ~ScopedTimer() { + float ms = m_Timer.ElapsedMillis(); + SE_CORE_TRACE_TAG("Timer", "{} - {:.3f}ms", m_Name, ms); + } + + private: + std::string m_Name; + Timer m_Timer; + }; + + class PerformanceProfiler { + public: + struct PerFrameData { + float Time = 0.0f; + uint32_t Samples = 0; + + PerFrameData() = default; + PerFrameData(float time) : Time(time), Samples(1) {} + + operator float() const { return Time; } + + PerFrameData& operator+=(float time) { + Time += time; + ++Samples; + return *this; + } + }; + + void SetPerFrameTiming(const char* name, float time) { - return std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - m_Start).count() * 0.001f * 0.001f * 0.001f; + std::scoped_lock lock(m_PerFrameDataMutex); + + if (m_PerFrameData.find(name) == m_PerFrameData.end()) + m_PerFrameData[name] = 0.0f; + + PerFrameData& data = m_PerFrameData[name]; + data.Time += time; + data.Samples++; + } + + void Clear() { + std::scoped_lock lock(m_PerFrameDataMutex); + m_PerFrameData.clear(); + } + + const std::unordered_map& GetPerFrameData() const { return m_PerFrameData; } + + private: + std::unordered_map m_PerFrameData; + inline static std::mutex m_PerFrameDataMutex; + }; + + class ScopePerfTimer { + public: + ScopePerfTimer(const char* name, PerformanceProfiler* profiler) + : m_Name(name), m_Profiler(profiler) { } - float Timer::ElapsedMillis() + ~ScopePerfTimer() { - return Elapsed() * 1000.0f; + float time = m_Timer.ElapsedMillis(); + m_Profiler->SetPerFrameTiming(m_Name, time); } private: - std::chrono::time_point m_Start; + const char* m_Name; + PerformanceProfiler* m_Profiler; + Timer m_Timer; }; + // TODO: Replace with proper context-aware profiler system + inline PerformanceProfiler& GetGlobalProfiler() { + static PerformanceProfiler s_GlobalProfiler; + return s_GlobalProfiler; + } + +#define CONCAT_IMPL(x, y) x##y +#define CONCAT(x, y) CONCAT_IMPL(x, y) + +#if 1 + // TEMPORARY FIX: Use global profiler instead of Application singleton +#define SE_SCOPE_PERF(name) \ + ::StarEngine::ScopePerfTimer CONCAT(scope_perf_, __LINE__)(name, &::StarEngine::GetGlobalProfiler()) + +#define SE_SCOPE_TIMER(name) \ + ::StarEngine::ScopedTimer CONCAT(scoped_timer_, __LINE__)(name) +#else +#define SE_SCOPE_PERF(name) +#define SE_SCOPE_TIMER(name) +#endif + } diff --git a/StarEngine/src/StarEngine/Core/Timestep.h b/StarEngine/src/StarEngine/Core/Timestep.h index e8403172..3d136b6b 100644 --- a/StarEngine/src/StarEngine/Core/Timestep.h +++ b/StarEngine/src/StarEngine/Core/Timestep.h @@ -4,15 +4,17 @@ namespace StarEngine { class Timestep { - public: - Timestep(float time = 0.0f) - : m_Time(time) - { - } - operator float() const { return m_Time; } - float GetSeconds() const { return m_Time; } - float GetMilliseconds() const { return m_Time * 1000.0f; } - private: - float m_Time; + public: + Timestep() = default; + Timestep(float time) + : m_Time(time) { + } + + inline float GetSeconds() const { return m_Time; } + inline float GetMilliseconds() const { return m_Time * 1000.0f; } + + operator float() const { return m_Time; } + private: + float m_Time = 0.0f; }; -} \ No newline at end of file +} diff --git a/StarEngine/src/StarEngine/Core/UUID.cpp b/StarEngine/src/StarEngine/Core/UUID.cpp index c25c475f..f4992f4f 100644 --- a/StarEngine/src/StarEngine/Core/UUID.cpp +++ b/StarEngine/src/StarEngine/Core/UUID.cpp @@ -3,22 +3,38 @@ #include -#include - namespace StarEngine { static std::random_device s_RandomDevice; - static std::mt19937_64 s_Engine(s_RandomDevice()); + static std::mt19937_64 eng(s_RandomDevice()); static std::uniform_int_distribution s_UniformDistribution; + static std::mt19937 eng32(s_RandomDevice()); + static std::uniform_int_distribution s_UniformDistribution32; + UUID::UUID() - : m_UUID(s_UniformDistribution(s_Engine)) - { - } + : m_UUID(s_UniformDistribution(eng)) + {} UUID::UUID(uint64_t uuid) : m_UUID(uuid) - { - } + {} + + UUID::UUID(const UUID& other) + : m_UUID(other.m_UUID) + {} + + + UUID32::UUID32() + : m_UUID(s_UniformDistribution32(eng32)) + {} + + UUID32::UUID32(uint32_t uuid) + : m_UUID(uuid) + {} + + UUID32::UUID32(const UUID32& other) + : m_UUID(other.m_UUID) + {} } diff --git a/StarEngine/src/StarEngine/Core/UUID.h b/StarEngine/src/StarEngine/Core/UUID.h index d11e3218..5ac798a6 100644 --- a/StarEngine/src/StarEngine/Core/UUID.h +++ b/StarEngine/src/StarEngine/Core/UUID.h @@ -1,31 +1,61 @@ #pragma once +#include "Base.h" + namespace StarEngine { + // "UUID" (universally unique identifier) or GUID is (usually) a 128-bit integer + // used to "uniquely" identify information. In StarEngine, even though we use the term + // GUID and UUID, at the moment we're simply using a randomly generated 64-bit + // integer, as the possibility of a clash is low enough for now. + // This may change in the future. class UUID { public: UUID(); UUID(uint64_t uuid); - UUID(const UUID&) = default; + UUID(const UUID& other); - operator uint64_t() const { return m_UUID; } + operator uint64_t () { return m_UUID; } + operator const uint64_t() const { return m_UUID; } private: uint64_t m_UUID; }; + class UUID32 + { + public: + UUID32(); + UUID32(uint32_t uuid); + UUID32(const UUID32& other); + + operator uint32_t () { return m_UUID; } + operator const uint32_t() const { return m_UUID; } + private: + uint32_t m_UUID; + }; + } namespace std { - template struct hash; - template<> + template <> struct hash { std::size_t operator()(const StarEngine::UUID& uuid) const { - return (uint64_t)uuid; + // uuid is already a randomly generated number, and is suitable as a hash key as-is. + // this may change in future, in which case return hash{}(uuid); might be more appropriate + return uuid; } }; + template <> + struct hash + { + std::size_t operator()(const StarEngine::UUID32& uuid) const + { + return hash()((uint32_t)uuid); + } + }; } diff --git a/StarEngine/src/StarEngine/Core/Version.h b/StarEngine/src/StarEngine/Core/Version.h new file mode 100644 index 00000000..b7babc1d --- /dev/null +++ b/StarEngine/src/StarEngine/Core/Version.h @@ -0,0 +1,29 @@ +#pragma once + +#define SE_VERSION "2025.1" + +// +// Build Configuration +// +#if defined(SE_DEBUG) +#define SE_BUILD_CONFIG_NAME "Debug" +#elif defined(SE_RELEASE) +#define SE_BUILD_CONFIG_NAME "Release" +#elif defined(SE_DIST) +#define SE_BUILD_CONFIG_NAME "Dist" +#else +#error Undefined configuration? +#endif + +// +// Build Platform +// +#if defined(SE_PLATFORM_WINDOWS) +#define SE_BUILD_PLATFORM_NAME "Windows x64" +#elif defined(SE_PLATFORM_LINUX) +#define SAE_BUILD_PLATFORM_NAME "Linux" +#else +#define SE_BUILD_PLATFORM_NAME "Unknown" +#endif + +#define SE_VERSION_LONG "StarEngine " SE_VERSION " (" SE_BUILD_PLATFORM_NAME " " SE_BUILD_CONFIG_NAME ")" diff --git a/StarEngine/src/StarEngine/Core/Window.cpp b/StarEngine/src/StarEngine/Core/Window.cpp index 9bbda7ea..33783edd 100644 --- a/StarEngine/src/StarEngine/Core/Window.cpp +++ b/StarEngine/src/StarEngine/Core/Window.cpp @@ -1,19 +1,624 @@ #include "sepch.h" -#include "StarEngine/Core/Window.h" +#include "Window.h" -#ifdef SE_PLATFORM_WINDOWS -#include "Platform/Windows/WindowsWindow.h" -#endif +#include "StarEngine/Core/Events/ApplicationEvent.h" +#include "StarEngine/Core/Events/KeyEvent.h" +#include "StarEngine/Core/Events/MouseEvent.h" +#include "StarEngine/Core/Input.h" + +#include "StarEngine/Renderer/RendererAPI.h" + +#include "StarEngine/Platform/Vulkan/VulkanContext.h" +#include "StarEngine/Platform/Vulkan/VulkanSwapChain.h" +#include "StarEngine/Platform/Vulkan/VulkanDeviceManager.h" + +#include +#include + +#include + +namespace StarEngine { + + static const struct + { + nvrhi::Format format; + uint32_t redBits; + uint32_t greenBits; + uint32_t blueBits; + uint32_t alphaBits; + uint32_t depthBits; + uint32_t stencilBits; + } formatInfo[] = { + { nvrhi::Format::UNKNOWN, 0, 0, 0, 0, 0, 0, }, + { nvrhi::Format::R8_UINT, 8, 0, 0, 0, 0, 0, }, + { nvrhi::Format::RG8_UINT, 8, 8, 0, 0, 0, 0, }, + { nvrhi::Format::RG8_UNORM, 8, 8, 0, 0, 0, 0, }, + { nvrhi::Format::R16_UINT, 16, 0, 0, 0, 0, 0, }, + { nvrhi::Format::R16_UNORM, 16, 0, 0, 0, 0, 0, }, + { nvrhi::Format::R16_FLOAT, 16, 0, 0, 0, 0, 0, }, + { nvrhi::Format::RGBA8_UNORM, 8, 8, 8, 8, 0, 0, }, + { nvrhi::Format::RGBA8_SNORM, 8, 8, 8, 8, 0, 0, }, + { nvrhi::Format::BGRA8_UNORM, 8, 8, 8, 8, 0, 0, }, + { nvrhi::Format::SRGBA8_UNORM, 8, 8, 8, 8, 0, 0, }, + { nvrhi::Format::SBGRA8_UNORM, 8, 8, 8, 8, 0, 0, }, + { nvrhi::Format::R10G10B10A2_UNORM, 10, 10, 10, 2, 0, 0, }, + { nvrhi::Format::R11G11B10_FLOAT, 11, 11, 10, 0, 0, 0, }, + { nvrhi::Format::RG16_UINT, 16, 16, 0, 0, 0, 0, }, + { nvrhi::Format::RG16_FLOAT, 16, 16, 0, 0, 0, 0, }, + { nvrhi::Format::R32_UINT, 32, 0, 0, 0, 0, 0, }, + { nvrhi::Format::R32_FLOAT, 32, 0, 0, 0, 0, 0, }, + { nvrhi::Format::RGBA16_FLOAT, 16, 16, 16, 16, 0, 0, }, + { nvrhi::Format::RGBA16_UNORM, 16, 16, 16, 16, 0, 0, }, + { nvrhi::Format::RGBA16_SNORM, 16, 16, 16, 16, 0, 0, }, + { nvrhi::Format::RG32_UINT, 32, 32, 0, 0, 0, 0, }, + { nvrhi::Format::RG32_FLOAT, 32, 32, 0, 0, 0, 0, }, + { nvrhi::Format::RGB32_UINT, 32, 32, 32, 0, 0, 0, }, + { nvrhi::Format::RGB32_FLOAT, 32, 32, 32, 0, 0, 0, }, + { nvrhi::Format::RGBA32_UINT, 32, 32, 32, 32, 0, 0, }, + { nvrhi::Format::RGBA32_FLOAT, 32, 32, 32, 32, 0, 0, }, + }; + + static void GLFWErrorCallback(int error, const char* description) + { + SE_CORE_ERROR_TAG("GLFW", "GLFW Error ({0}): {1}", error, description); + } + + static bool s_GLFWInitialized = false; + + Window* Window::Create(const WindowSpecification& specification) + { + return new Window(specification); + } -namespace StarEngine -{ - Scope Window::Create(const WindowProps& props) + Window::Window(const WindowSpecification& props) + : m_Specification(props) { + } + + Window::~Window() + { + Shutdown(); + } + + void Window::Init() + { + m_Data.Title = m_Specification.Title; + m_Data.Width = m_Specification.Width; + m_Data.Height = m_Specification.Height; + + DeviceCreationParameters deviceParams; + deviceParams.Decorated = m_Specification.Decorated; + deviceParams.swapChainBufferCount = 3; + deviceParams.enableRayTracingExtensions = true; + deviceParams.maxFramesInFlight = 1; + deviceParams.backBufferWidth = m_Specification.Width; + deviceParams.backBufferHeight = m_Specification.Height; + deviceParams.vsyncEnabled = false; + deviceParams.enableDebugRuntime = true; + deviceParams.ignoredVulkanValidationMessageLocations = { 0xc81ad50e }; + + SE_CORE_INFO_TAG("GLFW", "Creating window {0} ({1}, {2})", m_Specification.Title, m_Specification.Width, m_Specification.Height); + + if (!s_GLFWInitialized) + { + // TODO: glfwTerminate on system shutdown + int success = glfwInit(); + SE_CORE_ASSERT(success, "Could not intialize GLFW!"); + glfwSetErrorCallback(GLFWErrorCallback); + + s_GLFWInitialized = true; + } + + if (RendererAPI::Current() == RendererAPIType::Vulkan) + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + +#ifdef _WINDOWS + if (params.enablePerMonitorDPI) + { + // this needs to happen before glfwInit in order to override GLFW behavior + SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + } + else + { + SetProcessDpiAwareness(PROCESS_DPI_UNAWARE); + } +#endif + + glfwDefaultWindowHints(); + + bool foundFormat = false; + for (const auto& info : formatInfo) + { + if (info.format == deviceParams.swapChainFormat) + { + glfwWindowHint(GLFW_RED_BITS, info.redBits); + glfwWindowHint(GLFW_GREEN_BITS, info.greenBits); + glfwWindowHint(GLFW_BLUE_BITS, info.blueBits); + glfwWindowHint(GLFW_ALPHA_BITS, info.alphaBits); + glfwWindowHint(GLFW_DEPTH_BITS, info.depthBits); + glfwWindowHint(GLFW_STENCIL_BITS, info.stencilBits); + foundFormat = true; + break; + } + } + + assert(foundFormat); + + glfwWindowHint(GLFW_SAMPLES, deviceParams.swapChainSampleCount); + glfwWindowHint(GLFW_REFRESH_RATE, deviceParams.refreshRate); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // Ignored for fullscreen + + if (!m_Specification.Decorated) + { + // This removes titlebar on all platforms + // and all of the native window effects on non-Windows platforms #ifdef SE_PLATFORM_WINDOWS - return CreateScope(props); + glfwWindowHint(GLFW_TITLEBAR, false); #else - SE_CORE_ASSERT(false, "Unknown platform!"); - return nullptr; + glfwWindowHint(GLFW_DECORATED, false); #endif + } + + m_WindowHandle = glfwCreateWindow((int)m_Specification.Width, (int)m_Specification.Height, m_Data.Title.c_str(), m_Specification.Fullscreen ? glfwGetPrimaryMonitor() : nullptr, nullptr); + + glfwSetWindowUserPointer(m_WindowHandle, this); + + if (m_WindowHandle == nullptr) + { + // return false; + } + + if (m_Specification.Fullscreen) + { + glfwSetWindowMonitor(m_WindowHandle, glfwGetPrimaryMonitor(), 0, 0, + m_Specification.Width, m_Specification.Height, deviceParams.refreshRate); + } + else + { + int fbWidth = 0, fbHeight = 0; + glfwGetFramebufferSize(m_WindowHandle, &fbWidth, &fbHeight); + m_Data.Width = fbWidth; + m_Data.Height = fbHeight; + } + + if (deviceParams.windowPosX != -1 && deviceParams.windowPosY != -1) + { + glfwSetWindowPos(m_WindowHandle, deviceParams.windowPosX, deviceParams.windowPosY); + } + +#if TODO + if (m_Specification.StartMaximized) + { + glfwMaximizeWindow(m_WindowHandle); + } +#endif + + // Set icon + { + GLFWimage icon; + int channels; + + bool useEmbedded = m_Specification.IconPath.empty(); + + if (!useEmbedded) + { + std::string iconPathStr = m_Specification.IconPath.string(); + icon.pixels = stbi_load(iconPathStr.c_str(), &icon.width, &icon.height, &channels, 4); + if (icon.pixels) + { + glfwSetWindowIcon(m_WindowHandle, 1, &icon); + stbi_image_free(icon.pixels); + } + else + { + useEmbedded = true; + } + } + + if (useEmbedded) + { + // Use embedded StarEngine icon + icon.pixels = stbi_load_from_memory(g_StarIconPNG, sizeof(g_StarIconPNG), &icon.width, &icon.height, &channels, 4); + glfwSetWindowIcon(m_WindowHandle, 1, &icon); + stbi_image_free(icon.pixels); + } + } + + nvrhi::GraphicsAPI api = nvrhi::GraphicsAPI::VULKAN; + + m_DeviceManager = DeviceManager::Create(api, m_WindowHandle); + m_DeviceManager->SetWindowContext(this); + + if (!m_DeviceManager->CreateDevice(deviceParams, m_Specification.Title.c_str())) + { + SE_CORE_ERROR("Cannot initialize a {} graphics device.", (uint8_t)api); + return; + } + else + { + SE_CORE_INFO("Successfully created {} device!", (uint8_t)api); + } + + bool rayQuerySupported = m_DeviceManager->GetDevice()->queryFeatureSupport(nvrhi::Feature::RayQuery); + + if (!rayQuerySupported) + { + SE_CORE_ERROR("The GPU ({}) or its driver does not support Ray Queries.", m_DeviceManager->GetRendererString()); + return; + } + else + { + SE_CORE_INFO("rayQuerySupported=true"); + } + + if (!deviceParams.headlessDevice) + CreateWindowSurface(); + + m_DeviceManager->InitSurfaceCapabilities(*(uint64_t*)&m_WindowSurface); + + m_SwapChain = snew VulkanSwapChain(m_WindowSurface); + m_SwapChain->Create(m_Specification.Width, m_Specification.Height); + +#if OLD + // Create Renderer Context + m_RendererContext = RendererContext::Create(); + m_RendererContext->Init(); + + Ref context = m_RendererContext.As(); + + m_SwapChain = snew VulkanSwapChain(); + m_SwapChain->Init(VulkanContext::GetInstance(), context->GetDevice()); + m_SwapChain->InitSurface(m_WindowHandle); + + m_SwapChain->Create(&m_Data.Width, &m_Data.Height, m_Specification.VSync); +#endif + //glfwMaximizeWindow(m_Window); + glfwSetWindowUserPointer(m_WindowHandle, &m_Data); + + bool isRawMouseMotionSupported = glfwRawMouseMotionSupported(); + if (isRawMouseMotionSupported) + glfwSetInputMode(m_WindowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); + else + SE_CORE_WARN_TAG("Platform", "Raw mouse motion not supported."); + + // Set GLFW callbacks + glfwSetWindowSizeCallback(m_WindowHandle, [](GLFWwindow* window, int width, int height) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + + WindowResizeEvent event((uint32_t)width, (uint32_t)height); + data.EventCallback(event); + data.Width = width; + data.Height = height; + }); + + glfwSetWindowCloseCallback(m_WindowHandle, [](GLFWwindow* window) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + + WindowCloseEvent event; + data.EventCallback(event); + }); + + glfwSetKeyCallback(m_WindowHandle, [](GLFWwindow* window, int key, int scancode, int action, int mods) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + + switch (action) + { + case GLFW_PRESS: + { + Input::UpdateKeyState((KeyCode)key, KeyState::Pressed); + KeyPressedEvent event((KeyCode)key, 0); + data.EventCallback(event); + break; + } + case GLFW_RELEASE: + { + Input::UpdateKeyState((KeyCode)key, KeyState::Released); + KeyReleasedEvent event((KeyCode)key); + data.EventCallback(event); + break; + } + case GLFW_REPEAT: + { + Input::UpdateKeyState((KeyCode)key, KeyState::Held); + KeyPressedEvent event((KeyCode)key, 1); + data.EventCallback(event); + break; + } + } + }); + + glfwSetCharCallback(m_WindowHandle, [](GLFWwindow* window, uint32_t codepoint) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + + KeyTypedEvent event((KeyCode)codepoint); + data.EventCallback(event); + }); + + glfwSetMouseButtonCallback(m_WindowHandle, [](GLFWwindow* window, int button, int action, int mods) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + + switch (action) + { + case GLFW_PRESS: + { + Input::UpdateButtonState((MouseButton)button, KeyState::Pressed); + MouseButtonPressedEvent event((MouseButton)button); + data.EventCallback(event); + break; + } + case GLFW_RELEASE: + { + Input::UpdateButtonState((MouseButton)button, KeyState::Released); + MouseButtonReleasedEvent event((MouseButton)button); + data.EventCallback(event); + break; + } + } + }); + + glfwSetScrollCallback(m_WindowHandle, [](GLFWwindow* window, double xOffset, double yOffset) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + + MouseScrolledEvent event((float)xOffset, (float)yOffset); + data.EventCallback(event); + }); + + glfwSetCursorPosCallback(m_WindowHandle, [](GLFWwindow* window, double x, double y) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + MouseMovedEvent event((float)x, (float)y); + data.EventCallback(event); + }); + + glfwSetTitlebarHitTestCallback(m_WindowHandle, [](GLFWwindow* window, int x, int y, int* hit) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + WindowTitleBarHitTestEvent event(x, y, *hit); + data.EventCallback(event); + }); + + glfwSetWindowIconifyCallback(m_WindowHandle, [](GLFWwindow* window, int iconified) + { + auto& data = *((WindowData*)glfwGetWindowUserPointer(window)); + WindowMinimizeEvent event((bool)iconified); + data.EventCallback(event); + }); + + m_ImGuiMouseCursors[ImGuiMouseCursor_Arrow] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); + m_ImGuiMouseCursors[ImGuiMouseCursor_TextInput] = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR); + m_ImGuiMouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); // FIXME: GLFW doesn't have this. + m_ImGuiMouseCursors[ImGuiMouseCursor_ResizeNS] = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR); + m_ImGuiMouseCursors[ImGuiMouseCursor_ResizeEW] = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR); + m_ImGuiMouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); // FIXME: GLFW doesn't have this. + m_ImGuiMouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); // FIXME: GLFW doesn't have this. + m_ImGuiMouseCursors[ImGuiMouseCursor_Hand] = glfwCreateStandardCursor(GLFW_HAND_CURSOR); + + // Update window size to actual size + { + int width, height; + glfwGetWindowSize(m_WindowHandle, &width, &height); + m_Data.Width = width; + m_Data.Height = height; + } + + } + + bool Window::CreateWindowSurface() + { + const VkResult res = glfwCreateWindowSurface(((VulkanDeviceManager*)m_DeviceManager)->GetVulkanInstance(), m_WindowHandle, nullptr, (VkSurfaceKHR*)&m_WindowSurface); + if (res != VK_SUCCESS) + { + SE_CORE_ERROR("Failed to create a GLFW window surface, error code = {}", nvrhi::vulkan::resultToString(res)); + return false; + } + + return true; + } + + void Window::Shutdown() + { + if (m_WindowSurface) + { + auto vInstance = ((VulkanDeviceManager*)m_DeviceManager)->GetVulkanInstance(); + SE_CORE_VERIFY(vInstance); + vInstance.destroySurfaceKHR(m_WindowSurface); + m_WindowSurface = nullptr; + } + + m_DeviceManager->Shutdown(); + sdelete m_DeviceManager; + + // m_SwapChain->Destroy(); + // sdelete m_SwapChain; + // m_RendererContext.As()->GetDevice()->Destroy(); // need to destroy the device _before_ windows window destructor destroys the renderer context (because device Destroy() asks for renderer context...) + // glfwTerminate(); + s_GLFWInitialized = false; } -} \ No newline at end of file + + inline std::pair Window::GetWindowPos() const + { + int x, y; + glfwGetWindowPos(m_WindowHandle, &x, &y); + return { (float)x, (float)y }; + } + + void Window::ProcessEvents() + { + glfwPollEvents(); + Input::Update(); + + // m_DeviceManager->UpdateWindowSize(); + int width; + int height; + glfwGetWindowSize(m_WindowHandle, &width, &height); + + if (m_Data.Width != width || m_Data.Height != height) + { + m_Data.Width = width; + m_Data.Height = height; + + m_SwapChain->OnResize(width, height); + } + + } + + void Window::Present() + { + m_SwapChain->Present(); + } + + void Window::SetVSync(bool enabled) + { + m_Specification.VSync = enabled; + + Application::Get().QueueEvent([&]() + { + // m_SwapChain->SetVSync(m_Specification.VSync); + // m_SwapChain->OnResize(m_Specification.Width, m_Specification.Height); + }); + } + + bool Window::IsVSync() const + { + return m_Specification.VSync; + } + + void Window::SetResizable(bool resizable) const + { + glfwSetWindowAttrib(m_WindowHandle, GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE); + } + + void Window::BeginFrame() + { + SE_CORE_VERIFY(m_SwapChain); + m_SwapChain->BeginFrame(); + } + + void Window::Maximize() + { + glfwMaximizeWindow(m_WindowHandle); + } + + void Window::CenterWindow() + { + const GLFWvidmode* videmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + int x = (videmode->width / 2) - (m_Data.Width / 2); + int y = (videmode->height / 2) - (m_Data.Height / 2); + glfwSetWindowPos(m_WindowHandle, x, y); + } + + void Window::SetTitle(const std::string& title) + { + m_Data.Title = title; + glfwSetWindowTitle(m_WindowHandle, m_Data.Title.c_str()); + } + + VulkanSwapChain& Window::GetSwapChain() + { + return *m_SwapChain; + } + + void Window::OnWindowSizeCallback(int width, int height) + { + WindowResizeEvent event((uint32_t)width, (uint32_t)height); + if (m_Data.EventCallback) + m_Data.EventCallback(event); + m_Data.Width = width; + m_Data.Height = height; + } + + void Window::OnWindowCloseCallback() + { + WindowCloseEvent event; + m_Data.EventCallback(event); + } + + void Window::OnKeyCallback(int key, int scancode, int action, int mods) + { + switch (action) + { + case GLFW_PRESS: + { + Input::UpdateKeyState((KeyCode)key, KeyState::Pressed); + KeyPressedEvent event((KeyCode)key, 0); + m_Data.EventCallback(event); + break; + } + case GLFW_RELEASE: + { + Input::UpdateKeyState((KeyCode)key, KeyState::Released); + KeyReleasedEvent event((KeyCode)key); + m_Data.EventCallback(event); + break; + } + case GLFW_REPEAT: + { + Input::UpdateKeyState((KeyCode)key, KeyState::Held); + KeyPressedEvent event((KeyCode)key, 1); + m_Data.EventCallback(event); + break; + } + } + } + + void Window::OnCharCallback(uint32_t codepoint) + { + KeyTypedEvent event((KeyCode)codepoint); + m_Data.EventCallback(event); + } + + void Window::OnMouseButtonCallback(int button, int action, int mods) + { + switch (action) + { + case GLFW_PRESS: + { + Input::UpdateButtonState((MouseButton)button, KeyState::Pressed); + MouseButtonPressedEvent event((MouseButton)button); + m_Data.EventCallback(event); + break; + } + case GLFW_RELEASE: + { + Input::UpdateButtonState((MouseButton)button, KeyState::Released); + MouseButtonReleasedEvent event((MouseButton)button); + m_Data.EventCallback(event); + break; + } + } + } + + void Window::OnMouseScrollCallback(double xOffset, double yOffset) + { + MouseScrolledEvent event((float)xOffset, (float)yOffset); + m_Data.EventCallback(event); + } + + void Window::OnMousePosCallback(double x, double y) + { + MouseMovedEvent event((float)x, (float)y); + m_Data.EventCallback(event); + } + + void Window::OnTitlebarHitTestCallback(int x, int y, int* hit) + { + WindowTitleBarHitTestEvent event(x, y, *hit); + m_Data.EventCallback(event); + } + + void Window::OnWindowIconifyCallback(int iconified) + { + WindowMinimizeEvent event((bool)iconified); + m_Data.EventCallback(event); + } + +} diff --git a/StarEngine/src/StarEngine/Core/Window.h b/StarEngine/src/StarEngine/Core/Window.h index 9dfeccd2..d5f54ccd 100644 --- a/StarEngine/src/StarEngine/Core/Window.h +++ b/StarEngine/src/StarEngine/Core/Window.h @@ -1,49 +1,109 @@ #pragma once #include "StarEngine/Core/Base.h" -#include "StarEngine/Events/Event.h" +#include "StarEngine/Core/Events/Event.h" -#include +#include "StarEngine/Renderer/DeviceManager.h" +#include "StarEngine/Renderer/RendererContext.h" + +#include +#include + +#include // NOTE(Emily): This ensures that the first inclusion of GLFW defines +// Vulkan exclusive procs before include guards trip. + +#include +#include namespace StarEngine { - struct WindowProps + struct WindowSpecification { - std::string Title; - uint32_t Width; - uint32_t Height; - - WindowProps(const std::string& title = "StarEngine", - uint32_t width = 1600, - uint32_t height = 900) - : Title(title), Width(width), Height(height) - { - } + std::string Title = "StarEngine"; + uint32_t Width = 1600; + uint32_t Height = 900; + bool Decorated = true; + bool Fullscreen = false; + bool VSync = true; + std::filesystem::path IconPath; }; - // Interface representing a desktop system based Window + class VulkanSwapChain; + class Window { public: using EventCallbackFn = std::function; - virtual ~Window() {} + Window(const WindowSpecification& specification); + virtual ~Window(); + + virtual void Init(); + virtual void ProcessEvents(); + virtual void Present(); - virtual void OnUpdate() = 0; + inline uint32_t GetWidth() const { return m_Data.Width; } + inline uint32_t GetHeight() const { return m_Data.Height; } - virtual uint32_t GetWidth() const = 0; - virtual uint32_t GetHeight() const = 0; + virtual std::pair GetSize() const { return { m_Data.Width, m_Data.Height }; } + virtual std::pair GetWindowPos() const; // Window attributes - virtual void SetEventCallback(const EventCallbackFn& callback) = 0; - virtual void SetVSync(bool enabled) = 0; - virtual bool IsVSync() const = 0; + virtual void SetEventCallback(const EventCallbackFn& callback) { m_Data.EventCallback = callback; } + virtual void SetVSync(bool enabled); + virtual bool IsVSync() const; + virtual void SetResizable(bool resizable) const; + + void BeginFrame(); + + virtual void Maximize(); + virtual void CenterWindow(); - virtual void* GetNativeWindow() const = 0; + virtual const std::string& GetTitle() const { return m_Data.Title; } + virtual void SetTitle(const std::string& title); - static Scope Create(const WindowProps& props = WindowProps()); + inline GLFWwindow* GetNativeWindow() const { return m_WindowHandle; } + + virtual Ref GetRenderContext() { return m_RendererContext; } + virtual VulkanSwapChain& GetSwapChain(); + DeviceManager* GetDeviceManager() { return m_DeviceManager; } + + void OnWindowSizeCallback(int width, int height); + void OnWindowCloseCallback(); + void OnKeyCallback(int key, int scancode, int action, int mods); + void OnCharCallback(uint32_t codepoint); + void OnMouseButtonCallback(int button, int action, int mods); + void OnMouseScrollCallback(double xOffset, double yOffset); + void OnMousePosCallback(double x, double y); + void OnTitlebarHitTestCallback(int x, int y, int* hit); + void OnWindowIconifyCallback(int iconified); public: - static float s_HighDPIScaleFactor; + static Window* Create(const WindowSpecification& specification = WindowSpecification()); + private: + bool CreateWindowSurface(); + virtual void Shutdown(); + private: + DeviceManager* m_DeviceManager = nullptr; + GLFWwindow* m_WindowHandle = nullptr; + GLFWcursor* m_ImGuiMouseCursors[9] = { 0 }; + WindowSpecification m_Specification; + struct WindowData + { + std::string Title; + uint32_t Width, Height; + + EventCallbackFn EventCallback; + }; + + WindowData m_Data; + float m_LastFrameTime = 0.0f; + + Ref m_RendererContext; + VulkanSwapChain* m_SwapChain; + + vk::SurfaceKHR m_WindowSurface; + + friend class DeviceManager; }; } diff --git a/StarEngine/src/StarEngine/Debug/Instrumentor.h b/StarEngine/src/StarEngine/Debug/Instrumentor.h deleted file mode 100644 index 1a9f00ce..00000000 --- a/StarEngine/src/StarEngine/Debug/Instrumentor.h +++ /dev/null @@ -1,247 +0,0 @@ -#pragma once - -#include "StarEngine/Core/Log.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace StarEngine { - - using FloatingPointMicroseconds = std::chrono::duration; - - struct ProfileResult - { - std::string Name; - - FloatingPointMicroseconds Start; - std::chrono::microseconds ElapsedTime; - std::thread::id ThreadID; - }; - - struct InstrumentationSession - { - std::string Name; - }; - - class Instrumentor - { - public: - Instrumentor(const Instrumentor&) = delete; - Instrumentor(Instrumentor&&) = delete; - - void BeginSession(const std::string& name, const std::string& filepath = "results.json") - { - std::lock_guard lock(m_Mutex); - if (m_CurrentSession) - { - // If there is already a current session, then close it before beginning new one. - // Subsequent profiling output meant for the original session will end up in the - // newly opened session instead. That's better than having badly formatted - // profiling output. - if (Log::GetCoreLogger()) // Edge case: BeginSession() might be before Log::Init() - { - SE_CORE_ERROR("Instrumentor::BeginSession('{0}') when session '{1}' already open.", name, m_CurrentSession->Name); - } - InternalEndSession(); - } - m_OutputStream.open(filepath); - - if (m_OutputStream.is_open()) - { - m_CurrentSession = new InstrumentationSession({ name }); - WriteHeader(); - } - else - { - if (Log::GetCoreLogger()) // Edge case: BeginSession() might be before Log::Init() - { - SE_CORE_ERROR("Instrumentor could not open results file '{0}'.", filepath); - } - } - } - - void EndSession() - { - std::lock_guard lock(m_Mutex); - InternalEndSession(); - } - - void WriteProfile(const ProfileResult& result) - { - std::lock_guard lock(m_Mutex); - - std::stringstream json; - json << std::setprecision(3) << std::fixed; - json << ",{"; - json << "\"cat\":\"function\","; - json << "\"dur\":" << (result.ElapsedTime.count()) << ','; - json << "\"name\":\"" << result.Name << "\","; - json << "\"ph\":\"X\","; - json << "\"pid\":0,"; - json << "\"tid\":" << result.ThreadID << ","; - json << "\"ts\":" << result.Start.count(); - json << "}"; - - if (m_CurrentSession) - { - m_OutputStream << json.str(); - m_OutputStream.flush(); - } - } - - - static Instrumentor& Get() - { - static Instrumentor instance; - return instance; - } - - private: - - Instrumentor() - : m_CurrentSession(nullptr) - { - - } - - ~Instrumentor() - { - EndSession(); - } - - void WriteHeader() - { - m_OutputStream << "{\"otherData\": {},\"traceEvents\":[{}"; - m_OutputStream.flush(); - } - - void WriteFooter() - { - m_OutputStream << "]}"; - m_OutputStream.flush(); - } - - // Note: you must already own lock on m_Mutex before - // calling InternalEndSession() - void InternalEndSession() - { - if (m_CurrentSession) - { - WriteFooter(); - m_OutputStream.close(); - delete m_CurrentSession; - m_CurrentSession = nullptr; - } - } - - private: - std::mutex m_Mutex; - InstrumentationSession* m_CurrentSession; - std::ofstream m_OutputStream; - }; - - class InstrumentationTimer - { - public: - InstrumentationTimer(const char* name) - : m_Name(name), m_Stopped(false) - { - m_StartTimepoint = std::chrono::steady_clock::now(); - } - - ~InstrumentationTimer() - { - if (!m_Stopped) - Stop(); - } - - void Stop() - { - auto endTimepoint = std::chrono::steady_clock::now(); - auto highResStart = FloatingPointMicroseconds{ m_StartTimepoint.time_since_epoch() }; - auto elapsedTime = std::chrono::time_point_cast(endTimepoint).time_since_epoch() - std::chrono::time_point_cast(m_StartTimepoint).time_since_epoch(); - - Instrumentor::Get().WriteProfile({ m_Name, highResStart, elapsedTime, std::this_thread::get_id() }); - - m_Stopped = true; - } - private: - const char* m_Name; - std::chrono::time_point m_StartTimepoint; - bool m_Stopped; - }; - - namespace InstrumentorUtils { - template - struct ChangeResult - { - char Data[N]; - }; - template - constexpr auto CleanupOutputString(const char(&expr)[N], const char(&remove)[K]) - { - ChangeResult result = {}; - size_t srcIndex = 0; - size_t dstIndex = 0; - while (srcIndex < N) - { - size_t matchIndex = 0; - while (matchIndex < K - 1 && srcIndex + matchIndex < N - 1 && expr[srcIndex + matchIndex] == remove[matchIndex]) - matchIndex++; - if (matchIndex == K - 1) - srcIndex += matchIndex; - result.Data[dstIndex++] = expr[srcIndex] == '"' ? '\'' : expr[srcIndex]; - srcIndex++; - } - return result; - } - } -} - -#define SE_PROFILE 0 -#if SE_PROFILE -// Resolve which function signature macro will be used. Note that this only -// is resolved when the (pre)compiler starts, so the syntax highlighting -// could mark the wrong one in your editor! -#if defined(__GNUC__) || (defined(__MWERKS__) && (__MWERKS__ >= 0x3000)) || (defined(__ICC) && (__ICC >= 600)) || defined(__ghs__) -#define SE_FUNC_SIG __PRETTY_FUNCTION__ -#elif defined(__DMC__) && (__DMC__ >= 0x810) -#define SE_FUNC_SIG __PRETTY_FUNCTION__ -#elif defined(__FUNCSIG__) -#elif (defined(__FUNCSIG__) || (_MSC_VER)) -#elif (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 600)) || (defined(__IBMCPP__) && (__IBMCPP__ >= 500)) -#define SE_FUNC_SIG __FUNCTION__ -#elif defined(__BORLANDC__) && (__BORLANDC__ >= 0x550) -#define SE_FUNC_SIG __FUNC__ -#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901) -#define SE_FUNC_SIG __func__ -#elif defined(__cplusplus) && (__cplusplus >= 201103) -#define SE_FUNC_SIG __func__ -#else -#define SE_FUNC_SIG "SE_FUNC_SIG unknown!" -#endif - -#define SE_PROFILE_BEGIN_SESSION(name, filepath) ::StarEngine::Instrumentor::Get().BeginSession(name, filepath) -#define SE_PROFILE_END_SESSION() ::StarEngine::Instrumentor::Get().EndSession() -#define SE_PROFILE_SCOPE_LINE2(name, line) constexpr auto fixedName##line = ::StarEngine::InstrumentorUtils::CleanupOutputString(name, "__cdecl ");\ - ::StarEngine::InstrumentationTimer timer##line(fixedName##line.Data) -#define SE_PROFILE_SCOPE_LINE(name, line) SE_PROFILE_SCOPE_LINE2(name, line) -#define SE_PROFILE_SCOPE(name) SE_PROFILE_SCOPE_LINE(name, __LINE__) -#define SE_PROFILE_SCOPE_COLOR(name, ...) SE_PROFILE_FUNCTION_COLOR(name, __VA_ARGS__) -#define SE_PROFILE_FUNCTION() SE_PROFILE_SCOPE(SS_FUNC_SIG) -#define SE_PROFILE_FUNCTION_COLOR(name, ...) ZoneScopedNC(name, __VA_ARGS__) // Color is in hexadecimal -#else -#define SE_PROFILE_BEGIN_SESSION(name, filepath) -#define SE_PROFILE_END_SESSION() -#define SE_PROFILE_SCOPE(name) -#define SE_PROFILE_SCOPE_COLOR(name, ...) -#define SE_PROFILE_FUNCTION() -#define SE_PROFILE_FUNCTION_COLOR(name, ...) -#endif diff --git a/StarEngine/src/StarEngine/Debug/Profiler.h b/StarEngine/src/StarEngine/Debug/Profiler.h new file mode 100644 index 00000000..c4388771 --- /dev/null +++ b/StarEngine/src/StarEngine/Debug/Profiler.h @@ -0,0 +1,28 @@ +#pragma once + +#define SE_ENABLE_PROFILING !SE_DIST + +#if SE_ENABLE_PROFILING +#include +#endif + + +#if SE_ENABLE_PROFILING +#define SE_PROFILE_MARK_FRAME FrameMark; +#define SE_PROFILE_FUNCTION(...) ZoneScopedN(__VA_ARGS__) +#define SE_PROFILE_FUNCTION_COLOR(name, ...) ZoneScopedNC(name, __VA_ARGS__) // Color is in hexadecimal +#define SE_PROFILE_SCOPE(...) SE_PROFILE_FUNCTION(__VA_ARGS__) +#define SE_PROFILE_SCOPE_COLOR(name, ...) SE_PROFILE_FUNCTION_COLOR(name, __VA_ARGS__) +#define SE_PROFILE_SCOPE_DYNAMIC(NAME) ZoneScoped; ZoneName(NAME, strlen(NAME)) +#define SE_PROFILE_THREAD(...) tracy::SetThreadName(__VA_ARGS__) +#define SE_PROFILE_GPU_SCOPE(...) TracyGpuZone(__VA_ARGS__) +#else +#define SE_PROFILE_MARK_FRAME +#define SE_PROFILE_FUNCTION(...) +#define SE_PROFILE_FUNCTION_COLOR(name, ...) +#define SE_PROFILE_SCOPE(...) +#define SE_PROFILE_SCOPE_COLOR(name, ...) +#define SE_PROFILE_SCOPE_DYNAMIC(NAME) +#define SE_PROFILE_THREAD(...) +#define SE_PROFILE_GPU_SCOPE(...) +#endif diff --git a/StarEngine/src/StarEngine/Editor/AssetEditorPanel.cpp b/StarEngine/src/StarEngine/Editor/AssetEditorPanel.cpp new file mode 100644 index 00000000..393741ba --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/AssetEditorPanel.cpp @@ -0,0 +1,147 @@ +#include "sepch.h" +#include "AssetEditorPanel.h" +#include "DefaultAssetEditors.h" + +#include "StarEngine/Asset/AssetManager.h" +//#include "StarEngine/Audio/SoundObject.h" +//#include "StarEngine/Editor/AnimationEditor.h" +//#include "StarEngine/Editor/NodeGraphEditor/AnimationGraph/AnimationGraphEditor.h" +//#include "StarEngine/Editor/NodeGraphEditor/SoundGraph/SoundGraphGraphEditor.h" + +#include "MeshViewerPanel.h" +#include "MeshColliderEditor.h" + +namespace StarEngine { + + AssetEditor::AssetEditor(const char* id) + : m_Id(id), m_MinSize(200, 400), m_MaxSize(2000, 2000) + { + SetTitle(id); + } + + void AssetEditor::OnImGuiRender() + { + if (!m_IsOpen) + return; + + bool was_open = m_IsOpen; + // NOTE(Peter): SetNextWindowSizeConstraints requires a max constraint that's above 0. For now we're just setting it to a large value + OnWindowStylePush(); + ImGui::SetNextWindowSizeConstraints(m_MinSize, m_MaxSize); + ImGui::Begin(m_TitleAndId.c_str(), &m_IsOpen, GetWindowFlags()); + OnWindowStylePop(); + if(m_IsOpen) { + Render(); + } + ImGui::End(); + + if (was_open && !m_IsOpen) + OnClose(); + } + + void AssetEditor::SetOpen(bool isOpen) + { + m_IsOpen = isOpen; + if (!m_IsOpen) + OnClose(); + else + OnOpen(); + } + + void AssetEditor::SetMinSize(uint32_t width, uint32_t height) + { + if (width <= 0) width = 200; + if (height <= 0) height = 400; + + m_MinSize = ImVec2(float(width), float(height)); + } + + void AssetEditor::SetMaxSize(uint32_t width, uint32_t height) + { + if (width <= 0) width = 2000; + if (height <= 0) height = 2000; + if (float(width) <= m_MinSize.x) width = (uint32_t)(m_MinSize.x * 2.f); + if (float(height) <= m_MinSize.y) height = (uint32_t)(m_MinSize.y * 2.f); + + m_MaxSize = ImVec2(float(width), float(height)); + } + + const std::string& AssetEditor::GetTitle() const + { + return m_Title; + } + + void AssetEditor::SetTitle(const std::string& newTitle) + { + m_Title = newTitle; + m_TitleAndId = newTitle + "###" + m_Id; + } + + void AssetEditorPanel::RegisterDefaultEditors() + { + RegisterEditor(AssetType::Material); + RegisterEditor(AssetType::Texture); + //RegisterEditor(AssetType::Audio); + //RegisterEditor(AssetType::MeshSource); + RegisterEditor(AssetType::Prefab); + //RegisterEditor(AssetType::SoundConfig); + RegisterEditor(AssetType::MeshCollider); + //RegisterEditor(AssetType::SoundGraphSound); + //RegisterEditor(AssetType::Animation); + //RegisterEditor(AssetType::AnimationGraph); + } + + void AssetEditorPanel::UnregisterAllEditors() + { + s_Editors.clear(); + } + + void AssetEditorPanel::OnUpdate(Timestep ts) + { + for (auto& kv : s_Editors) + kv.second->OnUpdate(ts); + } + + void AssetEditorPanel::OnEvent(Event& e) + { + for (auto& kv : s_Editors) + kv.second->OnEvent(e); + } + + void AssetEditorPanel::SetSceneContext(const Ref& context) + { + s_SceneContext = context; + + for (auto& kv : s_Editors) + kv.second->SetSceneContext(context); + } + + void AssetEditorPanel::OnImGuiRender() + { + for (auto& kv : s_Editors) + kv.second->OnImGuiRender(); + } + + void AssetEditorPanel::OpenEditor(const Ref& asset) + { + if (asset == nullptr) + return; + + if (s_Editors.find(asset->GetAssetType()) == s_Editors.end()) + return; + + auto& editor = s_Editors[asset->GetAssetType()]; + if (editor->IsOpen()) + editor->SetOpen(false); + + const auto& metadata = Project::GetEditorAssetManager()->GetMetadata(asset->Handle); + editor->SetTitle(metadata.FilePath.filename().string()); + editor->SetAsset(asset); + editor->SetSceneContext(s_SceneContext); + editor->SetOpen(true); + } + + std::unordered_map> AssetEditorPanel::s_Editors; + Ref AssetEditorPanel::s_SceneContext = nullptr; + +} diff --git a/StarEngine/src/StarEngine/Editor/AssetEditorPanel.h b/StarEngine/src/StarEngine/Editor/AssetEditorPanel.h new file mode 100644 index 00000000..8d1ce77d --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/AssetEditorPanel.h @@ -0,0 +1,77 @@ +#pragma once + +#include "StarEngine/ImGui/PropertyGrid.h" +#include "StarEngine/Core/TimeStep.h" +#include "StarEngine/Core/Events/Event.h" + +namespace StarEngine { + + class AssetEditor + { + protected: + AssetEditor(const char* id); + + public: + virtual ~AssetEditor() {} + + virtual void OnUpdate(Timestep ts) {} + virtual void OnEvent(Event& e) {} + virtual void SetSceneContext(const Ref& context) {} + virtual void OnImGuiRender(); + void SetOpen(bool isOpen); + virtual void SetAsset(const Ref& asset) = 0; + + bool IsOpen() const { return m_IsOpen; } + + const std::string& GetTitle() const; + void SetTitle(const std::string& newTitle); + + protected: + void SetMinSize(uint32_t width, uint32_t height); + void SetMaxSize(uint32_t width, uint32_t height); + + virtual ImGuiWindowFlags GetWindowFlags() { return 0; } + + // Subclass can optionally override these to customize window styling, e.g. window padding + virtual void OnWindowStylePush() {} + virtual void OnWindowStylePop() {} + + private: + virtual void OnOpen() = 0; + virtual void OnClose() = 0; + virtual void Render() = 0; + + protected: + std::string m_Id; // Uniquely identifies the window independently of its title (e.g. for imgui.ini settings) + std::string m_Title; + std::string m_TitleAndId; // Caches title###id to avoid computing it every frame + ImVec2 m_MinSize, m_MaxSize; + bool m_IsOpen = false; + + }; + + class AssetEditorPanel + { + public: + static void RegisterDefaultEditors(); + static void UnregisterAllEditors(); + static void OnUpdate(Timestep ts); + static void OnEvent(Event& e); + static void SetSceneContext(const Ref& context); + static void OnImGuiRender(); + static void OpenEditor(const Ref& asset); + + template + static void RegisterEditor(AssetType type) + { + static_assert(std::is_base_of::value, "AssetEditorPanel::RegisterEditor requires template type to inherit from AssetEditor"); + SE_CORE_ASSERT(s_Editors.find(type) == s_Editors.end(), "There's already an editor for that asset!"); + s_Editors[type] = CreateScope(); + } + + private: + static std::unordered_map> s_Editors; + static Ref s_SceneContext; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/AssetEditorPanelInterface.cpp b/StarEngine/src/StarEngine/Editor/AssetEditorPanelInterface.cpp new file mode 100644 index 00000000..2fd0174f --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/AssetEditorPanelInterface.cpp @@ -0,0 +1,15 @@ +#include "sepch.h" +#include "AssetEditorPanelInterface.h" +#include "AssetEditorPanel.h" + +namespace StarEngine { + + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + void AssetEditorPanelInterface::OpenEditor(const Ref& asset) + { + AssetEditorPanel::OpenEditor(asset); + } + +} + diff --git a/StarEngine/src/StarEngine/Editor/AssetEditorPanelInterface.h b/StarEngine/src/StarEngine/Editor/AssetEditorPanelInterface.h new file mode 100644 index 00000000..f61b2a4b --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/AssetEditorPanelInterface.h @@ -0,0 +1,16 @@ +#pragma once + +#include "StarEngine/Asset/Asset.h" + +namespace StarEngine { + + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + class AssetEditorPanelInterface + { + public: + + static void OpenEditor(const Ref& asset); + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/DefaultAssetEditors.cpp b/StarEngine/src/StarEngine/Editor/DefaultAssetEditors.cpp new file mode 100644 index 00000000..165281ac --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/DefaultAssetEditors.cpp @@ -0,0 +1,390 @@ +#include "sepch.h" +#include "DefaultAssetEditors.h" + +#include "StarEngine/Asset/AssetImporter.h" +#include "StarEngine/Asset/AssetManager.h" +//#include "StarEngine/Audio/AudioFileUtils.h" +//#include "StarEngine/Audio/Sound.h" +//#include "StarEngine/Editor/NodeGraphEditor/SoundGraph/SoundGraphAsset.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Editor/SelectionManager.h" +#include "StarEngine/Audio/AudioEngine.h" + +#include "imgui_internal.h" + +namespace StarEngine { + + MaterialEditor::MaterialEditor() + : AssetEditor("Material Editor") + { + } + + void MaterialEditor::OnOpen() + { + if (!m_MaterialAsset) + SetOpen(false); + } + + void MaterialEditor::OnClose() + { + m_MaterialAsset = nullptr; + } + + void MaterialEditor::Render() + { + SE_CORE_ASSERT(m_MaterialAsset); + + bool needsSerialize = false; + + bool transparent = m_MaterialAsset->IsTransparent(); + UI::BeginPropertyGrid(); + UI::PushID(); + if (UI::Property("Transparent", transparent)) + { + if (transparent) + m_MaterialAsset->SetMaterial(Material::Create(Renderer::GetShaderLibrary()->Get("HazelPBR_Transparent"))); + else + m_MaterialAsset->SetMaterial(Material::Create(Renderer::GetShaderLibrary()->Get("HazelPBR_Static"))); + + m_MaterialAsset->m_Transparent = transparent; + m_MaterialAsset->SetDefaults(); + needsSerialize = true; + } + UI::PopID(); + UI::EndPropertyGrid(); + + ImGui::Text("Shader: %s", m_MaterialAsset->GetMaterial()->GetShader()->GetName().c_str()); + + auto getDroppedTextureHandle = []() { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + if (data && data->DataSize / sizeof(AssetHandle) == 1) + { + AssetHandle assetHandle = *(((AssetHandle*)data->Data)); + Ref asset = AssetManager::GetAsset(assetHandle); + if (asset && asset->GetAssetType() == AssetType::Texture) + { + return asset->Handle; + } + } + return AssetHandle{ 0 }; + }; + + // Albedo + if (ImGui::CollapsingHeader("Albedo", nullptr, ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 10)); + + auto albedoColor = m_MaterialAsset->GetAlbedoColor(); + auto albedoMap = m_MaterialAsset->GetAlbedoMap(); + bool hasAlbedoMap = albedoMap ? !albedoMap.EqualsObject(Renderer::GetWhiteTexture()) && albedoMap->GetImage() : false; + Ref albedoUITexture = hasAlbedoMap ? albedoMap : EditorResources::CheckerboardTexture; + + ImVec2 textureCursorPos = ImGui::GetCursorPos(); + UI::Image(albedoUITexture, ImVec2(64, 64)); + if (ImGui::BeginDragDropTarget()) + { + if (auto handle = getDroppedTextureHandle(); handle != 0) + { + m_MaterialAsset->SetAlbedoMap(handle); + needsSerialize = true; + } + ImGui::EndDragDropTarget(); + } + + ImGui::PopStyleVar(); + if (ImGui::IsItemHovered()) + { + if (hasAlbedoMap) + { + UI::ImageToolTip(albedoMap); + } + if (ImGui::IsItemClicked()) + { + } + } + + ImVec2 nextRowCursorPos = ImGui::GetCursorPos(); + ImGui::SameLine(); + ImVec2 properCursorPos = ImGui::GetCursorPos(); + ImGui::SetCursorPos(textureCursorPos); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (hasAlbedoMap && ImGui::Button("X##AlbedoMap", ImVec2(18, 18))) + { + m_MaterialAsset->ClearAlbedoMap(); + needsSerialize = true; + } + ImGui::PopStyleVar(); + ImGui::SetCursorPos(properCursorPos); + + if (UI::ColorEdit3("##Albedo", glm::value_ptr(albedoColor), ImGuiColorEditFlags_NoInputs)) + m_MaterialAsset->SetAlbedoColor(albedoColor); + if (ImGui::IsItemDeactivated()) + needsSerialize = true; + ImGui::SameLine(); + ImGui::TextUnformatted("Color"); + + float& emissive = m_MaterialAsset->GetEmission(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(100.0f); + UI::DragFloat("##Emission", &emissive, 0.1f, 0.0f, 20.0f); + if (ImGui::IsItemDeactivated()) + needsSerialize = true; + ImGui::SameLine(); + ImGui::TextUnformatted("Emission"); + + ImGui::SetCursorPos(nextRowCursorPos); + } + + if (transparent) + { + float& transparency = m_MaterialAsset->GetTransparency(); + + UI::BeginPropertyGrid(); + UI::Property("Transparency", transparency, 0.01f, 0.0f, 1.0f); + if (ImGui::IsItemDeactivated()) + needsSerialize = true; + UI::EndPropertyGrid(); + } + else + { + // Textures ------------------------------------------------------------------------------ + { + // Normals + if (ImGui::CollapsingHeader("Normals", nullptr, ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 10)); + bool useNormalMap = m_MaterialAsset->IsUsingNormalMap(); + Ref normalMap = m_MaterialAsset->GetNormalMap(); + + bool hasNormalMap = normalMap ? !normalMap.EqualsObject(Renderer::GetWhiteTexture()) && normalMap->GetImage() : false; + ImVec2 textureCursorPos = ImGui::GetCursorPos(); + UI::Image(hasNormalMap ? normalMap : EditorResources::CheckerboardTexture, ImVec2(64, 64)); + + if (ImGui::BeginDragDropTarget()) + { + if (auto handle = getDroppedTextureHandle(); handle != 0) + { + m_MaterialAsset->SetNormalMap(handle); + m_MaterialAsset->SetUseNormalMap(true); + needsSerialize = true; + } + ImGui::EndDragDropTarget(); + } + + ImGui::PopStyleVar(); + if (ImGui::IsItemHovered()) + { + if (hasNormalMap) + { + UI::ImageToolTip(normalMap); + } + if (ImGui::IsItemClicked()) + { + } + } + ImVec2 nextRowCursorPos = ImGui::GetCursorPos(); + ImGui::SameLine(); + ImVec2 properCursorPos = ImGui::GetCursorPos(); + ImGui::SetCursorPos(textureCursorPos); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (hasNormalMap && ImGui::Button("X##NormalMap", ImVec2(18, 18))) + { + m_MaterialAsset->ClearNormalMap(); + needsSerialize = true; + } + ImGui::PopStyleVar(); + ImGui::SetCursorPos(properCursorPos); + + if (UI::Checkbox("##Use", &useNormalMap)) + m_MaterialAsset->SetUseNormalMap(useNormalMap); + if (ImGui::IsItemDeactivated()) + needsSerialize = true; + ImGui::SameLine(); + ImGui::TextUnformatted("Use"); + + ImGui::SetCursorPos(nextRowCursorPos); + } + } + { + // Metalness + if (ImGui::CollapsingHeader("Metalness", nullptr, ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 10)); + float& metalnessValue = m_MaterialAsset->GetMetalness(); + Ref metalnessMap = m_MaterialAsset->GetMetalnessMap(); + + bool hasMetalnessMap = metalnessMap ? !metalnessMap.EqualsObject(Renderer::GetWhiteTexture()) && metalnessMap->GetImage() : false; + ImVec2 textureCursorPos = ImGui::GetCursorPos(); + UI::Image(hasMetalnessMap ? metalnessMap : EditorResources::CheckerboardTexture, ImVec2(64, 64)); + + if (ImGui::BeginDragDropTarget()) + { + if (auto handle = getDroppedTextureHandle(); handle != 0) + { + m_MaterialAsset->SetMetalnessMap(handle); + needsSerialize = true; + } + ImGui::EndDragDropTarget(); + } + + ImGui::PopStyleVar(); + if (ImGui::IsItemHovered()) + { + if (hasMetalnessMap) + { + UI::ImageToolTip(metalnessMap); + } + if (ImGui::IsItemClicked()) + { + } + } + ImVec2 nextRowCursorPos = ImGui::GetCursorPos(); + ImGui::SameLine(); + ImVec2 properCursorPos = ImGui::GetCursorPos(); + ImGui::SetCursorPos(textureCursorPos); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (hasMetalnessMap && ImGui::Button("X##MetalnessMap", ImVec2(18, 18))) + { + m_MaterialAsset->ClearMetalnessMap(); + needsSerialize = true; + } + ImGui::PopStyleVar(); + ImGui::SetCursorPos(properCursorPos); + ImGui::SetNextItemWidth(200.0f); + UI::SliderFloat("##MetalnessInput", &metalnessValue, 0.0f, 1.0f); + if (ImGui::IsItemDeactivated()) + needsSerialize = true; + ImGui::SameLine(); + ImGui::TextUnformatted("Metalness Value"); + ImGui::SetCursorPos(nextRowCursorPos); + } + } + { + // Roughness + if (ImGui::CollapsingHeader("Roughness", nullptr, ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10, 10)); + float& roughnessValue = m_MaterialAsset->GetRoughness(); + Ref roughnessMap = m_MaterialAsset->GetRoughnessMap(); + bool hasRoughnessMap = roughnessMap ? !roughnessMap.EqualsObject(Renderer::GetWhiteTexture()) && roughnessMap->GetImage() : false; + ImVec2 textureCursorPos = ImGui::GetCursorPos(); + UI::Image(hasRoughnessMap ? roughnessMap : EditorResources::CheckerboardTexture, ImVec2(64, 64)); + + if (ImGui::BeginDragDropTarget()) + { + if (auto handle = getDroppedTextureHandle(); handle != 0) + { + m_MaterialAsset->SetRoughnessMap(handle); + needsSerialize = true; + } + ImGui::EndDragDropTarget(); + } + + ImGui::PopStyleVar(); + if (ImGui::IsItemHovered()) + { + if (hasRoughnessMap) + { + UI::ImageToolTip(roughnessMap); + } + if (ImGui::IsItemClicked()) + { + } + } + ImVec2 nextRowCursorPos = ImGui::GetCursorPos(); + ImGui::SameLine(); + ImVec2 properCursorPos = ImGui::GetCursorPos(); + ImGui::SetCursorPos(textureCursorPos); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (hasRoughnessMap && ImGui::Button("X##RoughnessMap", ImVec2(18, 18))) + { + m_MaterialAsset->ClearRoughnessMap(); + needsSerialize = true; + } + ImGui::PopStyleVar(); + ImGui::SetCursorPos(properCursorPos); + ImGui::SetNextItemWidth(200.0f); + UI::SliderFloat("##RoughnessInput", &roughnessValue, 0.0f, 1.0f); + if (ImGui::IsItemDeactivated()) + needsSerialize = true; + ImGui::SameLine(); + ImGui::TextUnformatted("Roughness Value"); + ImGui::SetCursorPos(nextRowCursorPos); + } + } + + UI::BeginPropertyGrid(); + + bool castsShadows = m_MaterialAsset->IsShadowCasting(); + if (UI::Property("Casts shadows", castsShadows)) + m_MaterialAsset->SetShadowCasting(castsShadows); + + UI::EndPropertyGrid(); + } + + if (needsSerialize && !AssetManager::IsMemoryAsset(m_MaterialAsset->Handle)) + AssetImporter::Serialize(m_MaterialAsset); + } + + TextureViewer::TextureViewer() + : AssetEditor("Edit Texture") + { + SetMinSize(200, 600); + SetMaxSize(500, 1000); + } + + void TextureViewer::OnOpen() + { + if (!m_Asset) + SetOpen(false); + } + + void TextureViewer::OnClose() + { + m_Asset = nullptr; + } + + void TextureViewer::Render() + { + float textureWidth = (float)m_Asset->GetWidth(); + float textureHeight = (float)m_Asset->GetHeight(); + //float bitsPerPixel = Texture::GetBPP(m_Asset->GetFormat()); + float imageSize = ImGui::GetWindowWidth() - 40; + imageSize = glm::min(imageSize, 500.0f); + + ImGui::SetCursorPosX(20); + UI::Image(m_Asset.As(), {imageSize, imageSize}); + + UI::BeginPropertyGrid(); + UI::BeginDisabled(); + UI::Property("Width", textureWidth); + UI::Property("Height", textureHeight); + // UI::Property("Bits", bitsPerPixel, 0.1f, 0.0f, 0.0f, true); // TODO: Format + UI::EndDisabled(); + UI::EndPropertyGrid(); + } + + PrefabEditor::PrefabEditor() + : AssetEditor("Prefab Editor"), m_SceneHierarchyPanel(nullptr, SelectionContext::PrefabEditor, false) + { + } + + void PrefabEditor::OnOpen() + { + SelectionManager::DeselectAll(); + } + + void PrefabEditor::OnClose() + { + SelectionManager::DeselectAll(SelectionContext::PrefabEditor); + } + + void PrefabEditor::Render() + { + // Need to do this in order to ensure that the scene hierarchy panel doesn't close immediately. + // There's been some structural changes since the addition of the PanelManager. + bool isOpen = true; + m_SceneHierarchyPanel.OnImGuiRender(isOpen); + } + +} diff --git a/StarEngine/src/StarEngine/Editor/DefaultAssetEditors.h b/StarEngine/src/StarEngine/Editor/DefaultAssetEditors.h new file mode 100644 index 00000000..6358815c --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/DefaultAssetEditors.h @@ -0,0 +1,94 @@ +#pragma once + +#include "AssetEditorPanel.h" +#include "StarEngine/Renderer/Mesh.h" + +#include "StarEngine/Scene/Prefab.h" +#include "StarEngine/Editor/SceneHierarchyPanel.h" + +namespace StarEngine { + + struct SoundConfig; + + class MaterialEditor : public AssetEditor + { + public: + MaterialEditor(); + + virtual void SetAsset(const Ref& asset) override { m_MaterialAsset = (Ref)asset; } + + private: + virtual void OnOpen() override; + virtual void OnClose() override; + virtual void Render() override; + + private: + Ref m_MaterialAsset; + }; + + class PrefabEditor : public AssetEditor + { + public: + PrefabEditor(); + + virtual void SetAsset(const Ref& asset) override { m_Prefab = (Ref)asset; m_SceneHierarchyPanel.SetSceneContext(m_Prefab->m_Scene); } + + private: + virtual void OnOpen() override; + virtual void OnClose() override; + virtual void Render() override; + + private: + Ref m_Prefab; + SceneHierarchyPanel m_SceneHierarchyPanel; + }; + + class TextureViewer : public AssetEditor + { + public: + TextureViewer(); + + virtual void SetAsset(const Ref& asset) override { m_Asset = (Ref)asset; } + + private: + virtual void OnOpen() override; + virtual void OnClose() override; + virtual void Render() override; + + private: + Ref m_Asset; + }; + /* + class AudioFileViewer : public AssetEditor + { + public: + AudioFileViewer(); + + virtual void SetAsset(const Ref& asset) override; + + private: + virtual void OnOpen() override; + virtual void OnClose() override; + virtual void Render() override; + + private: + Ref m_Asset; + }; + + class SoundConfigEditor : public AssetEditor + { + public: + SoundConfigEditor(); + + virtual void SetAsset(const Ref& asset) override; + + private: + virtual void OnOpen() override; + virtual void OnClose() override; + virtual void Render() override; + + private: + Ref m_Asset; + };*/ + +} diff --git a/StarEngine/src/StarEngine/Editor/ECSDebugPanel.cpp b/StarEngine/src/StarEngine/Editor/ECSDebugPanel.cpp new file mode 100644 index 00000000..81476d3d --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/ECSDebugPanel.cpp @@ -0,0 +1,40 @@ +#include "sepch.h" +#include "ECSDebugPanel.h" + +#include "StarEngine/ImGui/PropertyGrid.h" + +#include + +namespace StarEngine { + + ECSDebugPanel::ECSDebugPanel(Ref context) + : m_Context(context) + { + } + + ECSDebugPanel::~ECSDebugPanel() + { + } + + void ECSDebugPanel::OnImGuiRender(bool& open) + { + if (ImGui::Begin("ECS Debug", &open) && m_Context) + { + for (auto e : m_Context->m_Registry.view()) + { + Entity entity = { e, m_Context.Raw() }; + const auto& name = entity.Name(); + + std::string label = std::format("{0} - {1}", name, entity.GetUUID()); + + bool selected = SelectionManager::IsSelected(SelectionContext::Scene, entity.GetUUID()); + if (ImGui::Selectable(label.c_str(), &selected, 0)) + SelectionManager::Select(SelectionContext::Scene, entity.GetUUID()); + } + + } + + ImGui::End(); + } + +} diff --git a/StarEngine/src/StarEngine/Editor/ECSDebugPanel.h b/StarEngine/src/StarEngine/Editor/ECSDebugPanel.h new file mode 100644 index 00000000..c6d232fc --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/ECSDebugPanel.h @@ -0,0 +1,23 @@ +#pragma once + +#include "StarEngine/Scene/Scene.h" +#include "SelectionManager.h" + +#include "EditorPanel.h" + +namespace StarEngine { + + class ECSDebugPanel : public EditorPanel + { + public: + ECSDebugPanel(Ref context); + ~ECSDebugPanel(); + + virtual void OnImGuiRender(bool& open) override; + + virtual void SetSceneContext(const Ref& context) { m_Context = context; } + private: + Ref m_Context; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorApplicationSettings.cpp b/StarEngine/src/StarEngine/Editor/EditorApplicationSettings.cpp new file mode 100644 index 00000000..aa295c89 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorApplicationSettings.cpp @@ -0,0 +1,128 @@ +#include "sepch.h" +#include "EditorApplicationSettings.h" +#include "StarEngine/Utilities/FileSystem.h" + +#include + +#include +#include + +namespace StarEngine { + + static std::filesystem::path s_EditorSettingsPath; + + EditorApplicationSettings& EditorApplicationSettings::Get() + { + static EditorApplicationSettings s_Settings; + return s_Settings; + } + + void EditorApplicationSettingsSerializer::Init() + { + s_EditorSettingsPath = std::filesystem::absolute("Config"); + + if (!FileSystem::Exists(s_EditorSettingsPath)) + FileSystem::CreateDirectory(s_EditorSettingsPath); + s_EditorSettingsPath /= "EditorApplicationSettings.yaml"; + + LoadSettings(); + } + +#define SE_ENTER_GROUP(name) currentGroup = rootNode[name]; +#define SE_READ_VALUE(name, type, var, defaultValue) var = currentGroup[name].as(defaultValue) + + void EditorApplicationSettingsSerializer::LoadSettings() + { + // Generate default settings file if one doesn't exist + if (!FileSystem::Exists(s_EditorSettingsPath)) + { + SaveSettings(); + return; + } + + std::ifstream stream(s_EditorSettingsPath); + SE_CORE_VERIFY(stream); + std::stringstream ss; + ss << stream.rdbuf(); + + YAML::Node data = YAML::Load(ss.str()); + if (!data["EditorApplicationSettings"]) + return; + + YAML::Node rootNode = data["EditorApplicationSettings"]; + YAML::Node currentGroup = rootNode; + + auto& settings = EditorApplicationSettings::Get(); + + SE_ENTER_GROUP("StarEditor"); + { + SE_READ_VALUE("AdvancedMode", bool, settings.AdvancedMode, false); + SE_READ_VALUE("HighlightUnsetMeshes", bool, settings.HighlightUnsetMeshes, true); + SE_READ_VALUE("TranslationSnapValue", float, settings.TranslationSnapValue, 0.5f); + SE_READ_VALUE("RotationSnapValue", float, settings.RotationSnapValue, 45.0f); + SE_READ_VALUE("ScaleSnapValue", float, settings.ScaleSnapValue, 0.5f); + } + + SE_ENTER_GROUP("Scripting"); + { + SE_READ_VALUE("ShowHiddenFields", bool, settings.ShowHiddenFields, false); + SE_READ_VALUE("DebuggerListenPort", int, settings.ScriptDebuggerListenPort, 2550); + } + + SE_ENTER_GROUP("ContentBrowser"); + { + SE_READ_VALUE("ShowAssetTypes", bool, settings.ContentBrowserShowAssetTypes, true); + SE_READ_VALUE("ThumbnailSize", int, settings.ContentBrowserThumbnailSize, 128); + } + + stream.close(); + } + +#define SE_BEGIN_GROUP(name)\ + out << YAML::Key << name << YAML::Value << YAML::BeginMap; +#define SE_END_GROUP() out << YAML::EndMap; + +#define SE_SERIALIZE_VALUE(name, value) out << YAML::Key << name << YAML::Value << value; + + void EditorApplicationSettingsSerializer::SaveSettings() + { + const auto& settings = EditorApplicationSettings::Get(); + + YAML::Emitter out; + out << YAML::BeginMap; + SE_BEGIN_GROUP("EditorApplicationSettings"); + { + SE_BEGIN_GROUP("StarEditor"); + { + SE_SERIALIZE_VALUE("AdvancedMode", settings.AdvancedMode); + SE_SERIALIZE_VALUE("HighlightUnsetMeshes", settings.HighlightUnsetMeshes); + SE_SERIALIZE_VALUE("TranslationSnapValue", settings.TranslationSnapValue); + SE_SERIALIZE_VALUE("RotationSnapValue", settings.RotationSnapValue); + SE_SERIALIZE_VALUE("ScaleSnapValue", settings.ScaleSnapValue); + } + SE_END_GROUP(); + + SE_BEGIN_GROUP("Scripting"); + { + SE_SERIALIZE_VALUE("ShowHiddenFields", settings.ShowHiddenFields); + SE_SERIALIZE_VALUE("DebuggerListenPort", settings.ScriptDebuggerListenPort); + } + SE_END_GROUP(); + + SE_BEGIN_GROUP("ContentBrowser"); + { + SE_SERIALIZE_VALUE("ShowAssetTypes", settings.ContentBrowserShowAssetTypes); + SE_SERIALIZE_VALUE("ThumbnailSize", settings.ContentBrowserThumbnailSize); + } + SE_END_GROUP(); + } + SE_END_GROUP(); + out << YAML::EndMap; + + std::ofstream fout(s_EditorSettingsPath); + fout << out.c_str(); + fout.close(); + } + + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorApplicationSettings.h b/StarEngine/src/StarEngine/Editor/EditorApplicationSettings.h new file mode 100644 index 00000000..e4d25273 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorApplicationSettings.h @@ -0,0 +1,34 @@ +#pragma once + +namespace StarEngine { + + struct EditorApplicationSettings + { + //---------- Scripting ------------ + bool ShowHiddenFields = false; + int ScriptDebuggerListenPort = 2550; + + //---------- Content Browser ------------ + bool ContentBrowserShowAssetTypes = true; + int ContentBrowserThumbnailSize = 128; + + //---------- Hazelnut ------------ + bool AdvancedMode = false; + bool HighlightUnsetMeshes = true; + float TranslationSnapValue = 0.5f; + float RotationSnapValue = 45.0f; + float ScaleSnapValue = 0.5f; + + static EditorApplicationSettings& Get(); + }; + + class EditorApplicationSettingsSerializer + { + public: + static void Init(); + + static void LoadSettings(); + static void SaveSettings(); + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorCamera.cpp b/StarEngine/src/StarEngine/Editor/EditorCamera.cpp new file mode 100644 index 00000000..e0d18ae9 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorCamera.cpp @@ -0,0 +1,274 @@ +#include "sepch.h" +#include "EditorCamera.h" + +#include "StarEngine/Core/Input.h" + +#include +#include + +#include "StarEngine/Core/Application.h" +#include "StarEngine/ImGui/PropertyGrid.h" + +namespace StarEngine { + + EditorCamera::EditorCamera(const float degFov, const float width, const float height, const float nearP, const float farP) + : Camera(glm::perspectiveFov(glm::radians(degFov), width, height, farP, nearP), glm::perspectiveFov(glm::radians(degFov), width, height, nearP, farP)), m_FocalPoint(0.0f), m_VerticalFOV(glm::radians(degFov)), m_NearClip(nearP), m_FarClip(farP) + { + Init(); + } + + void EditorCamera::Init() + { + constexpr glm::vec3 position = { -5, 5, 5 }; + m_Distance = glm::distance(position, m_FocalPoint); + + m_Yaw = 3.0f * glm::pi() / 4.0f; + m_Pitch = glm::pi() / 4.0f; + + m_Position = CalculatePosition(); + const glm::quat orientation = GetOrientation(); + m_Direction = glm::eulerAngles(orientation) * (180.0f / glm::pi()); + m_ViewMatrix = glm::translate(glm::mat4(1.0f), m_Position) * glm::toMat4(orientation); + m_ViewMatrix = glm::inverse(m_ViewMatrix); + } + + static void DisableMouse() + { + Input::SetCursorMode(CursorMode::Locked); + UI::SetInputEnabled(false); + } + + static void EnableMouse() + { + Input::SetCursorMode(CursorMode::Normal); + UI::SetInputEnabled(true); + } + + void EditorCamera::OnUpdate(const Timestep ts) + { + const glm::vec2& mouse{ Input::GetMouseX(), Input::GetMouseY() }; + const glm::vec2 delta = (mouse - m_InitialMousePosition) * 0.002f; + + if (!m_IsActive) + { + if (!UI::IsInputEnabled()) + UI::SetInputEnabled(true); + + return; + } + + if (Input::IsMouseButtonDown(MouseButton::Right) && !Input::IsKeyDown(KeyCode::LeftAlt)) + { + m_CameraMode = CameraMode::FLYCAM; + DisableMouse(); + const float yawSign = GetUpDirection().y < 0 ? -1.0f : 1.0f; + + const float speed = GetCameraSpeed(); + + if (Input::IsKeyDown(KeyCode::Q)) + m_PositionDelta -= ts.GetMilliseconds() * speed * glm::vec3{ 0.f, yawSign, 0.f }; + if (Input::IsKeyDown(KeyCode::E)) + m_PositionDelta += ts.GetMilliseconds() * speed * glm::vec3{ 0.f, yawSign, 0.f }; + if (Input::IsKeyDown(KeyCode::S)) + m_PositionDelta -= ts.GetMilliseconds() * speed * m_Direction; + if (Input::IsKeyDown(KeyCode::W)) + m_PositionDelta += ts.GetMilliseconds() * speed * m_Direction; + if (Input::IsKeyDown(KeyCode::A)) + m_PositionDelta -= ts.GetMilliseconds() * speed * m_RightDirection; + if (Input::IsKeyDown(KeyCode::D)) + m_PositionDelta += ts.GetMilliseconds() * speed * m_RightDirection; + + constexpr float maxRate{ 0.12f }; + m_YawDelta += glm::clamp(yawSign * delta.x * RotationSpeed(), -maxRate, maxRate); + m_PitchDelta += glm::clamp(delta.y * RotationSpeed(), -maxRate, maxRate); + + m_RightDirection = glm::cross(m_Direction, glm::vec3{ 0.f, yawSign, 0.f }); + + m_Direction = glm::rotate(glm::normalize(glm::cross(glm::angleAxis(-m_PitchDelta, m_RightDirection), + glm::angleAxis(-m_YawDelta, glm::vec3{ 0.f, yawSign, 0.f }))), m_Direction); + + const float distance = glm::distance(m_FocalPoint, m_Position); + m_FocalPoint = m_Position + GetForwardDirection() * distance; + m_Distance = distance; + } + else if (Input::IsKeyDown(KeyCode::LeftAlt)) + { + m_CameraMode = CameraMode::ARCBALL; + + if (Input::IsMouseButtonDown(MouseButton::Middle)) + { + DisableMouse(); + MousePan(delta); + } + else if (Input::IsMouseButtonDown(MouseButton::Left)) + { + DisableMouse(); + MouseRotate(delta); + } + else if (Input::IsMouseButtonDown(MouseButton::Right)) + { + DisableMouse(); + MouseZoom((delta.x + delta.y) * 0.1f); + } + else + EnableMouse(); + } + else + { + EnableMouse(); + } + + m_InitialMousePosition = mouse; + m_Position += m_PositionDelta; + m_Yaw += m_YawDelta; + m_Pitch += m_PitchDelta; + + if (m_CameraMode == CameraMode::ARCBALL) + m_Position = CalculatePosition(); + + UpdateCameraView(); + } + + float EditorCamera::GetCameraSpeed() const + { + float speed = m_NormalSpeed; + if (Input::IsKeyDown(KeyCode::LeftControl)) + speed /= 2 - glm::log(m_NormalSpeed); + if (Input::IsKeyDown(KeyCode::LeftShift)) + speed *= 2 - glm::log(m_NormalSpeed); + + return glm::clamp(speed, MIN_SPEED, MAX_SPEED); + } + + void EditorCamera::UpdateCameraView() + { + const float yawSign = GetUpDirection().y < 0 ? -1.0f : 1.0f; + + // Extra step to handle the problem when the camera direction is the same as the up vector + const float cosAngle = glm::dot(GetForwardDirection(), GetUpDirection()); + if (cosAngle * yawSign > 0.99f) + m_PitchDelta = 0.f; + + const glm::vec3 lookAt = m_Position + GetForwardDirection(); + m_Direction = glm::normalize(lookAt - m_Position); + m_Distance = glm::distance(m_Position, m_FocalPoint); + m_ViewMatrix = glm::lookAt(m_Position, lookAt, glm::vec3{ 0.f, yawSign, 0.f }); + + //damping for smooth camera + m_YawDelta *= 0.6f; + m_PitchDelta *= 0.6f; + m_PositionDelta *= 0.8f; + } + + void EditorCamera::Focus(const glm::vec3& focusPoint) + { + m_FocalPoint = focusPoint; + m_CameraMode = CameraMode::FLYCAM; + if (m_Distance > m_MinFocusDistance) + { + m_Distance -= m_Distance - m_MinFocusDistance; + m_Position = m_FocalPoint - GetForwardDirection() * m_Distance; + } + m_Position = m_FocalPoint - GetForwardDirection() * m_Distance; + UpdateCameraView(); + } + + std::pair EditorCamera::PanSpeed() const + { + const float x = glm::min(float(m_ViewportRight - m_ViewportLeft) / 1000.0f, 2.4f); // max = 2.4f + const float xFactor = 0.0366f * (x * x) - 0.1778f * x + 0.3021f; + + const float y = glm::min(float(m_ViewportBottom - m_ViewportTop) / 1000.0f, 2.4f); // max = 2.4f + const float yFactor = 0.0366f * (y * y) - 0.1778f * y + 0.3021f; + + return { xFactor, yFactor }; + } + + float EditorCamera::RotationSpeed() const + { + return 0.3f; + } + + float EditorCamera::ZoomSpeed() const + { + float distance = m_Distance * 0.2f; + distance = glm::max(distance, 0.0f); + float speed = distance * distance; + speed = glm::min(speed, 50.0f); // max speed = 50 + return speed; + } + + void EditorCamera::OnEvent(Event& event) + { + EventDispatcher dispatcher(event); + dispatcher.Dispatch([this](MouseScrolledEvent& e) { return OnMouseScroll(e); }); + } + + bool EditorCamera::OnMouseScroll(MouseScrolledEvent& e) + { + if (Input::IsMouseButtonDown(MouseButton::Right)) + { + m_NormalSpeed += e.GetYOffset() * 0.3f * m_NormalSpeed; + m_NormalSpeed = std::clamp(m_NormalSpeed, MIN_SPEED, MAX_SPEED); + } + else + { + MouseZoom(e.GetYOffset() * 0.1f); + UpdateCameraView(); + } + + return true; + } + + void EditorCamera::MousePan(const glm::vec2& delta) + { + auto [xSpeed, ySpeed] = PanSpeed(); + m_FocalPoint -= GetRightDirection() * delta.x * xSpeed * m_Distance; + m_FocalPoint += GetUpDirection() * delta.y * ySpeed * m_Distance; + } + + void EditorCamera::MouseRotate(const glm::vec2& delta) + { + const float yawSign = GetUpDirection().y < 0.0f ? -1.0f : 1.0f; + m_YawDelta += yawSign * delta.x * RotationSpeed(); + m_PitchDelta += delta.y * RotationSpeed(); + } + + void EditorCamera::MouseZoom(float delta) + { + m_Distance -= delta * ZoomSpeed(); + const glm::vec3 forwardDir = GetForwardDirection(); + m_Position = m_FocalPoint - forwardDir * m_Distance; + if (m_Distance < 1.0f) + { + m_FocalPoint += forwardDir * m_Distance; + m_Distance = 1.0f; + } + m_PositionDelta += delta * ZoomSpeed() * forwardDir; + } + + glm::vec3 EditorCamera::GetUpDirection() const + { + return glm::rotate(GetOrientation(), glm::vec3(0.0f, 1.0f, 0.0f)); + } + + glm::vec3 EditorCamera::GetRightDirection() const + { + return glm::rotate(GetOrientation(), glm::vec3(1.f, 0.f, 0.f)); + } + + glm::vec3 EditorCamera::GetForwardDirection() const + { + return glm::rotate(GetOrientation(), glm::vec3(0.0f, 0.0f, -1.0f)); + } + + glm::vec3 EditorCamera::CalculatePosition() const + { + return m_FocalPoint - GetForwardDirection() * m_Distance + m_PositionDelta; + } + + glm::quat EditorCamera::GetOrientation() const + { + return glm::quat(glm::vec3(-m_Pitch - m_PitchDelta, -m_Yaw - m_YawDelta, 0.0f)); + } +} diff --git a/StarEngine/src/StarEngine/Editor/EditorCamera.h b/StarEngine/src/StarEngine/Editor/EditorCamera.h new file mode 100644 index 00000000..69cfd78b --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorCamera.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include "StarEngine/Renderer/Camera.h" +#include "StarEngine/Core/TimeStep.h" +#include "StarEngine/Core/Events/KeyEvent.h" +#include "StarEngine/Core/Events/MouseEvent.h" + +namespace StarEngine { + + enum class CameraMode + { + NONE, FLYCAM, ARCBALL + }; + + class EditorCamera : public Camera + { + public: + EditorCamera(const float degFov, const float width, const float height, const float nearP, const float farP); + void Init(); + + void Focus(const glm::vec3& focusPoint); + void OnUpdate(Timestep ts); + void OnEvent(Event& e); + + bool IsActive() const { return m_IsActive; } + void SetActive(bool active) { m_IsActive = active; } + + CameraMode GetCurrentMode() const { return m_CameraMode; } + + inline float GetDistance() const { return m_Distance; } + inline void SetDistance(float distance) { m_Distance = distance; } + + const glm::vec3& GetFocalPoint() const { return m_FocalPoint; } + + inline void SetViewportBounds(uint32_t left, uint32_t top, uint32_t right, uint32_t bottom) + { + if (m_ViewportLeft == left && m_ViewportTop == top && m_ViewportRight == right && m_ViewportBottom == bottom) + return; + + if ((right - left) != (m_ViewportRight - m_ViewportLeft) || (bottom - top) != (m_ViewportBottom - m_ViewportTop)) + { + float width = (float)(right - left); + float height = (float)(bottom - top); + if (width > 0.0f && height > 0.0f) + { + SetPerspectiveProjectionMatrix(m_VerticalFOV, width, height, m_NearClip, m_FarClip); + } + } + + m_ViewportLeft = left; + m_ViewportTop = top; + m_ViewportRight = right; + m_ViewportBottom = bottom; + } + + const glm::mat4& GetViewMatrix() const { return m_ViewMatrix; } + glm::mat4 GetViewProjection() const { return GetProjectionMatrix() * m_ViewMatrix; } + glm::mat4 GetUnReversedViewProjection() const { return GetUnReversedProjectionMatrix() * m_ViewMatrix; } + + glm::vec3 GetUpDirection() const; + glm::vec3 GetRightDirection() const; + glm::vec3 GetForwardDirection() const; + + const glm::vec3& GetPosition() const { return m_Position; } + + glm::quat GetOrientation() const; + + [[nodiscard]] float GetVerticalFOV() const { return m_VerticalFOV; } + [[nodiscard]] float GetAspectRatio() const { return m_AspectRatio; } + [[nodiscard]] float GetNearClip() const { return m_NearClip; } + [[nodiscard]] float GetFarClip() const { return m_FarClip; } + [[nodiscard]] float GetPitch() const { return m_Pitch; } + [[nodiscard]] float GetYaw() const { return m_Yaw; } + [[nodiscard]] float GetCameraSpeed() const; + private: + void UpdateCameraView(); + + bool OnMouseScroll(MouseScrolledEvent& e); + + void MousePan(const glm::vec2& delta); + void MouseRotate(const glm::vec2& delta); + void MouseZoom(float delta); + + glm::vec3 CalculatePosition() const; + + std::pair PanSpeed() const; + float RotationSpeed() const; + float ZoomSpeed() const; + private: + glm::mat4 m_ViewMatrix; + glm::vec3 m_Position, m_Direction, m_FocalPoint; + + // Perspective projection params + float m_VerticalFOV, m_AspectRatio, m_NearClip, m_FarClip; + + bool m_IsActive = false; + bool m_Panning, m_Rotating; + glm::vec2 m_InitialMousePosition {}; + glm::vec3 m_InitialFocalPoint, m_InitialRotation; + + float m_Distance; + float m_NormalSpeed{ 0.002f }; + + float m_Pitch, m_Yaw; + float m_PitchDelta{}, m_YawDelta{}; + glm::vec3 m_PositionDelta{}; + glm::vec3 m_RightDirection{}; + + CameraMode m_CameraMode{ CameraMode::ARCBALL }; + + float m_MinFocusDistance{ 100.0f }; + + uint32_t m_ViewportLeft = 0; + uint32_t m_ViewportTop = 0; + uint32_t m_ViewportRight = 1280; + uint32_t m_ViewportBottom = 720; + + constexpr static float MIN_SPEED{ 0.0005f }, MAX_SPEED{ 2.0f }; + friend class EditorLayer; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorConsole/ConsoleMessage.h b/StarEngine/src/StarEngine/Editor/EditorConsole/ConsoleMessage.h new file mode 100644 index 00000000..711ecee3 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorConsole/ConsoleMessage.h @@ -0,0 +1,29 @@ +#pragma once + +#include "StarEngine/Core/Base.h" + +#include +#include + +namespace StarEngine { + + enum class ConsoleMessageFlags : int16_t + { + None = -1, + Info = BIT(0), + Warning = BIT(1), + Error = BIT(2), + + All = Info | Warning | Error + }; + + struct ConsoleMessage + { + std::string ShortMessage; + std::string LongMessage; + int16_t Flags; + + time_t Time; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorConsole/EditorConsoleSink.h b/StarEngine/src/StarEngine/Editor/EditorConsole/EditorConsoleSink.h new file mode 100644 index 00000000..e4be48e3 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorConsole/EditorConsoleSink.h @@ -0,0 +1,88 @@ +#pragma once + +#include "StarEngine/Editor/EditorConsolePanel.h" + +#include + +#include +#include + +namespace StarEngine { + + class EditorConsoleSink : public spdlog::sinks::base_sink + { + public: + explicit EditorConsoleSink(uint32_t bufferCapacity) + : m_MessageBufferCapacity(bufferCapacity), m_MessageBuffer(bufferCapacity) {} + + virtual ~EditorConsoleSink() = default; + + EditorConsoleSink(const EditorConsoleSink& other) = delete; + EditorConsoleSink& operator=(const EditorConsoleSink& other) = delete; + + protected: + void sink_it_(const spdlog::details::log_msg& msg) override + { + spdlog::memory_buf_t formatted; + spdlog::sinks::base_sink::formatter_->format(msg, formatted); + std::string longMessage = formatted; + std::string shortMessage = longMessage; + + if (shortMessage.length() > 100) + { + size_t spacePos = shortMessage.find_first_of(' ', 100); + if (spacePos != std::string::npos) + shortMessage.replace(spacePos, shortMessage.length() - 1, "..."); + } + + m_MessageBuffer[m_MessageCount++] = ConsoleMessage{ shortMessage, longMessage, GetMessageFlags(msg.level), std::chrono::system_clock::to_time_t(msg.time) }; + + if (m_MessageCount == m_MessageBufferCapacity) + flush_(); + } + + void flush_() override + { + for (const auto& message : m_MessageBuffer) + EditorConsolePanel::PushMessage(message); + + m_MessageCount = 0; + } + + private: + static int16_t GetMessageFlags(spdlog::level::level_enum level) + { + int16_t flags = 0; + + switch (level) + { + case spdlog::level::trace: + case spdlog::level::debug: + case spdlog::level::info: + { + flags |= (int16_t)ConsoleMessageFlags::Info; + break; + } + case spdlog::level::warn: + { + flags |= (int16_t)ConsoleMessageFlags::Warning; + break; + } + case spdlog::level::err: + case spdlog::level::critical: + { + flags |= (int16_t)ConsoleMessageFlags::Error; + break; + } + } + + return flags; + } + + private: + uint32_t m_MessageBufferCapacity; + std::vector m_MessageBuffer; + uint32_t m_MessageCount = 0; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorConsolePanel.cpp b/StarEngine/src/StarEngine/Editor/EditorConsolePanel.cpp new file mode 100644 index 00000000..711c996c --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorConsolePanel.cpp @@ -0,0 +1,257 @@ +#include "sepch.h" +#include "EditorConsolePanel.h" + +#include "StarEngine/Core/Application.h" +#include "StarEngine/Core/Events/SceneEvents.h" +#include "StarEngine/Editor/EditorResources.h" +#include "StarEngine/Editor/FontAwesome.h" +#include "StarEngine/ImGui/Colors.h" +#include "StarEngine/ImGui/PropertyGrid.h" + +#include + +#include + +namespace StarEngine { + + static EditorConsolePanel* s_Instance = nullptr; + + static const ImVec4 s_InfoTint = ImVec4(0.0f, 0.431372549f, 1.0f, 1.0f); + static const ImVec4 s_WarningTint = ImVec4(1.0f, 0.890196078f, 0.0588235294f, 1.0f); + static const ImVec4 s_ErrorTint = ImVec4(1.0f, 0.309803922f, 0.309803922f, 1.0f); + + EditorConsolePanel::EditorConsolePanel() + { + SE_CORE_ASSERT(s_Instance == nullptr); + s_Instance = this; + + m_MessageBuffer.reserve(500); + } + + EditorConsolePanel::~EditorConsolePanel() + { + s_Instance = nullptr; + } + + void EditorConsolePanel::OnEvent(Event& event) + { + EventDispatcher dispatcher(event); + dispatcher.Dispatch([this](ScenePreStartEvent& e) + { + if (m_ClearOnPlay) + { + std::scoped_lock lock(m_MessageBufferMutex); + m_MessageBuffer.clear(); + } + return false; + }); + } + + void EditorConsolePanel::OnImGuiRender(bool& isOpen) + { + if (ImGui::Begin(m_PanelName, &isOpen)) + { + ImVec2 consoleSize = ImGui::GetContentRegionAvail(); + consoleSize.y -= 32.0f; + + RenderMenu({ consoleSize.x, 28.0f }); + RenderConsole(consoleSize); + } + ImGui::End(); + } + + void EditorConsolePanel::OnProjectChanged(const Ref& project) + { + std::scoped_lock lock(m_MessageBufferMutex); + m_MessageBuffer.clear(); + } + + void EditorConsolePanel::Focus() + { + ImGui::SetWindowFocus(m_PanelName); + } + + void EditorConsolePanel::SetProgress(const std::string& label, float progress) + { + m_ProgressLabel = label; + m_Progress = progress; + + if (m_Progress >= 1.0f) + ClearProgress(); + } + + void EditorConsolePanel::ClearProgress() + { + m_ProgressLabel = std::string(); + m_Progress = 0.0f; + } + + void EditorConsolePanel::RenderMenu(const ImVec2& size) + { + UI::ScopedStyleStack frame(ImGuiStyleVar_FrameBorderSize, 0.0f, ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::BeginChild("Toolbar", size); + + const float ToolbarHeight = 28.0f; + + if (ImGui::Button("Clear", { 75.0f, ToolbarHeight })) + { + std::scoped_lock lock(m_MessageBufferMutex); + m_MessageBuffer.clear(); + } + + ImGui::SameLine(); + + const auto& style = ImGui::GetStyle(); + const std::string clearOnPlayText = std::format("{} Clear on Play", m_ClearOnPlay ? SE_ICON_CHECK : SE_ICON_TIMES); + ImVec4 textColor = m_ClearOnPlay ? style.Colors[ImGuiCol_Text] : style.Colors[ImGuiCol_TextDisabled]; + if (UI::ColoredButton(clearOnPlayText.c_str(), GetToolbarButtonColor(m_ClearOnPlay), textColor, ImVec2(110.0f, ToolbarHeight))) + m_ClearOnPlay = !m_ClearOnPlay; + + if (!m_ProgressLabel.empty()) + { + ImGui::SameLine(); + std::string progressBarText = std::format("{} ({}%)", m_ProgressLabel, (int)(m_Progress * 100 + 0.01f)); + ImGui::ProgressBar(m_Progress, ImVec2(ImGui::GetContentRegionAvail().x / 2.0f, ToolbarHeight), progressBarText.c_str()); + } + + { + const ImVec2 buttonSize(ToolbarHeight, ToolbarHeight); + + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 100.0f, 0.0f); + textColor = (m_MessageFilters & (int16_t)ConsoleMessageFlags::Info) ? s_InfoTint : style.Colors[ImGuiCol_TextDisabled]; + if (UI::ColoredButton(SE_ICON_INFO_CIRCLE, GetToolbarButtonColor(m_MessageFilters & (int16_t)ConsoleMessageFlags::Info), textColor, buttonSize)) + m_MessageFilters ^= (int16_t)ConsoleMessageFlags::Info; + + ImGui::SameLine(); + textColor = (m_MessageFilters & (int16_t)ConsoleMessageFlags::Warning) ? s_WarningTint : style.Colors[ImGuiCol_TextDisabled]; + if (UI::ColoredButton(SE_ICON_EXCLAMATION_TRIANGLE, GetToolbarButtonColor(m_MessageFilters & (int16_t)ConsoleMessageFlags::Warning), textColor, buttonSize)) + m_MessageFilters ^= (int16_t)ConsoleMessageFlags::Warning; + + ImGui::SameLine(); + textColor = (m_MessageFilters & (int16_t)ConsoleMessageFlags::Error) ? s_ErrorTint : style.Colors[ImGuiCol_TextDisabled]; + if (UI::ColoredButton(SE_ICON_EXCLAMATION_CIRCLE, GetToolbarButtonColor(m_MessageFilters & (int16_t)ConsoleMessageFlags::Error), textColor, buttonSize)) + m_MessageFilters ^= (int16_t)ConsoleMessageFlags::Error; + } + + ImGui::EndChild(); + } + + void EditorConsolePanel::RenderConsole(const ImVec2& size) + { + static const char* s_Columns[] = { "Type", "Timestamp", "Message" }; + + UI::Table("Console", s_Columns, 3, size, [&]() + { + std::scoped_lock lock(m_MessageBufferMutex); + + float scrollY = ImGui::GetScrollY(); + if (scrollY < m_PreviousScrollY) + m_EnableScrollToLatest = false; + + if (scrollY >= ImGui::GetScrollMaxY()) + m_EnableScrollToLatest = true; + + m_PreviousScrollY = scrollY; + + float rowHeight = 24.0f; + for (uint32_t i = 0; i < m_MessageBuffer.size(); i++) + { + const auto& msg = m_MessageBuffer[i]; + + if (!(m_MessageFilters & (int16_t)msg.Flags)) + continue; + + ImGui::PushID(&msg); + + const bool clicked = UI::TableRowClickable(msg.ShortMessage.c_str(), rowHeight); + + UI::Separator(ImVec2(4.0f, ImGui::CalcTextSize(msg.ShortMessage.c_str()).y), GetMessageColor(msg)); + ImGui::SameLine(); + ImGui::Text(GetMessageType(msg)); + ImGui::TableNextColumn(); + UI::ShiftCursorX(4.0f); + + std::stringstream timeString; + tm* timeBuffer = localtime(&msg.Time); + timeString << std::put_time(timeBuffer, "%T"); + ImGui::Text(timeString.str().c_str()); + + ImGui::TableNextColumn(); + UI::ShiftCursorX(4.0f); + ImGui::Text(msg.ShortMessage.c_str()); + + if (i == m_MessageBuffer.size() - 1 && m_ScrollToLatest) + { + ImGui::ScrollToItem(); + m_ScrollToLatest = false; + } + + if (clicked) + { + ImGui::OpenPopup("Detailed Message"); + auto[width, height] = Application::Get().GetWindow().GetSize(); + auto[xPos, yPos] = Application::Get().GetWindow().GetWindowPos(); + //ImVec2 size = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowSize({ (float)width * 0.5f, (float)height * 0.5f }); + ImGui::SetNextWindowPos({ xPos + (float)width / 2.0f, yPos + (float)height / 2.5f }, 0, { 0.5, 0.5 }); + m_DetailedPanelOpen = true; + } + + if (m_DetailedPanelOpen) + { + UI::ScopedStyle windowPadding(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f)); + UI::ScopedStyle framePadding(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 8.0f)); + + if (ImGui::BeginPopupModal("Detailed Message", &m_DetailedPanelOpen, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) + { + ImGui::TextWrapped(msg.LongMessage.c_str()); + if (ImGui::Button("Copy To Clipboard", ImVec2(120.0f, 28.0f))) + { + ImGui::SetClipboardText(msg.LongMessage.c_str()); + } + ImGui::EndPopup(); + } + } + + ImGui::PopID(); + } + }); + } + + const char* EditorConsolePanel::GetMessageType(const ConsoleMessage& message) const + { + if (message.Flags & (int16_t)ConsoleMessageFlags::Info) return "Info"; + if (message.Flags & (int16_t)ConsoleMessageFlags::Warning) return "Warning"; + if (message.Flags & (int16_t)ConsoleMessageFlags::Error) return "Error"; + return "Unknown"; + } + + const ImVec4& EditorConsolePanel::GetMessageColor(const ConsoleMessage& message) const + { + //if (message.Flags & (int16_t)ConsoleMessageFlags::Info) return s_InfoButtonOnTint; + if (message.Flags & (int16_t)ConsoleMessageFlags::Warning) return s_WarningTint; + if (message.Flags & (int16_t)ConsoleMessageFlags::Error) return s_ErrorTint; + return s_InfoTint; + } + + ImVec4 EditorConsolePanel::GetToolbarButtonColor(const bool value) const + { + const auto& style = ImGui::GetStyle(); + return value ? style.Colors[ImGuiCol_Header] : style.Colors[ImGuiCol_FrameBg]; + } + + void EditorConsolePanel::PushMessage(const ConsoleMessage& message) + { + if (s_Instance == nullptr) + return; + + { + std::scoped_lock lock(s_Instance->m_MessageBufferMutex); + s_Instance->m_MessageBuffer.push_back(message); + } + + if (s_Instance->m_EnableScrollToLatest) + s_Instance->m_ScrollToLatest = true; + } + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorConsolePanel.h b/StarEngine/src/StarEngine/Editor/EditorConsolePanel.h new file mode 100644 index 00000000..49924782 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorConsolePanel.h @@ -0,0 +1,58 @@ +#pragma once + +#include "EditorPanel.h" + +#include "EditorConsole/ConsoleMessage.h" + +#include "StarEngine/Renderer/Texture.h" + +#include + +namespace StarEngine { + + class EditorConsolePanel : public EditorPanel + { + public: + EditorConsolePanel(); + ~EditorConsolePanel(); + + virtual void OnEvent(Event& e) override; + virtual void OnImGuiRender(bool& isOpen) override; + virtual void OnProjectChanged(const Ref& project) override; + + void Focus(); + + void SetProgress(const std::string& label, float progress); + void ClearProgress(); + private: + void RenderMenu(const ImVec2& size); + void RenderConsole(const ImVec2& size); + const char* GetMessageType(const ConsoleMessage& message) const; + const ImVec4& GetMessageColor(const ConsoleMessage& message) const; + ImVec4 GetToolbarButtonColor(const bool value) const; + + private: + static void PushMessage(const ConsoleMessage& message); + + private: + const char* m_PanelName = "Log"; + bool m_ClearOnPlay = true; + + std::mutex m_MessageBufferMutex; + std::vector m_MessageBuffer; + + bool m_EnableScrollToLatest = true; + bool m_ScrollToLatest = false; + float m_PreviousScrollY = 0.0f; + + int16_t m_MessageFilters = (int16_t)ConsoleMessageFlags::All; + + bool m_DetailedPanelOpen = false; + + std::string m_ProgressLabel; + float m_Progress = 0.0f; + private: + friend class EditorConsoleSink; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorPanel.h b/StarEngine/src/StarEngine/Editor/EditorPanel.h new file mode 100644 index 00000000..3606ece4 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorPanel.h @@ -0,0 +1,21 @@ +#pragma once + +#include "StarEngine/Core/Ref.h" +#include "StarEngine/Scene/Scene.h" +#include "StarEngine/Project/Project.h" +#include "StarEngine/Core/Events/Event.h" + +namespace StarEngine { + + class EditorPanel : public RefCounted + { + public: + virtual ~EditorPanel() = default; + + virtual void OnImGuiRender(bool& isOpen) = 0; + virtual void OnEvent(Event& e) {} + virtual void OnProjectChanged(const Ref& project){} + virtual void SetSceneContext(const Ref& context){} + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/EditorResources.h b/StarEngine/src/StarEngine/Editor/EditorResources.h new file mode 100644 index 00000000..a471f2ca --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/EditorResources.h @@ -0,0 +1,349 @@ +#pragma once + +#include "StarEngine/Renderer/Texture.h" +#include "StarEngine/Utilities/FileSystem.h" + +namespace StarEngine { + + class EditorResources + { + public: + // Generic + inline static Ref GearIcon = nullptr; + inline static Ref PlusIcon = nullptr; + inline static Ref PencilIcon = nullptr; + inline static Ref ForwardIcon = nullptr; + inline static Ref BackIcon = nullptr; + inline static Ref PointerIcon = nullptr; + inline static Ref SearchIcon = nullptr; + inline static Ref ClearIcon = nullptr; + inline static Ref SaveIcon = nullptr; + inline static Ref ReticuleIcon = nullptr; + inline static Ref LinkedIcon = nullptr; + inline static Ref UnlinkedIcon = nullptr; + + // Icons + inline static Ref AnimationIcon = nullptr; + inline static Ref AssetIcon = nullptr; + inline static Ref AudioIcon = nullptr; + inline static Ref AudioListenerIcon = nullptr; + inline static Ref BoxColliderIcon = nullptr; + inline static Ref BoxCollider2DIcon = nullptr; + inline static Ref CameraIcon = nullptr; + inline static Ref CapsuleColliderIcon = nullptr; + inline static Ref CharacterControllerIcon = nullptr; + inline static Ref CircleCollider2DIcon = nullptr; + inline static Ref DirectionalLightIcon = nullptr; + inline static Ref FixedJointIcon = nullptr; + inline static Ref MeshIcon = nullptr; + inline static Ref MeshColliderIcon = nullptr; + inline static Ref PointLightIcon = nullptr; + inline static Ref RigidBodyIcon = nullptr; + inline static Ref RigidBody2DIcon = nullptr; + inline static Ref ScriptIcon = nullptr; + inline static Ref SkeletonIcon = nullptr; + inline static Ref SpriteIcon = nullptr; + inline static Ref SkyLightIcon = nullptr; + inline static Ref CompoundColliderIcon = nullptr; + inline static Ref SphereColliderIcon = nullptr; + inline static Ref SpotLightIcon = nullptr; + inline static Ref StaticMeshIcon = nullptr; + inline static Ref TextIcon = nullptr; + inline static Ref TransformIcon = nullptr; + inline static Ref RefreshIcon = nullptr; + + // Viewport + inline static Ref PlayIcon = nullptr; + inline static Ref PauseIcon = nullptr; + inline static Ref StopIcon = nullptr; + inline static Ref SimulateIcon = nullptr; + inline static Ref MoveIcon = nullptr; + inline static Ref RotateIcon = nullptr; + inline static Ref ScaleIcon = nullptr; + + // Window + inline static Ref MinimizeIcon = nullptr; + inline static Ref MaximizeIcon = nullptr; + inline static Ref RestoreIcon = nullptr; + inline static Ref CloseIcon = nullptr; + + // Content Browser + inline static Ref FolderIcon = nullptr; + inline static Ref FileIcon = nullptr; + inline static Ref FBXFileIcon = nullptr; + inline static Ref OBJFileIcon = nullptr; + inline static Ref GLTFFileIcon = nullptr; + inline static Ref GLBFileIcon = nullptr; + inline static Ref WAVFileIcon = nullptr; + inline static Ref MP3FileIcon = nullptr; + inline static Ref OGGFileIcon = nullptr; + inline static Ref CSFileIcon = nullptr; + inline static Ref PNGFileIcon = nullptr; + inline static Ref JPGFileIcon = nullptr; + inline static Ref MaterialFileIcon = nullptr; + inline static Ref SceneFileIcon = nullptr; + inline static Ref PrefabFileIcon = nullptr; + inline static Ref FontFileIcon = nullptr; + inline static Ref AnimationFileIcon = nullptr; + inline static Ref AnimationGraphFileIcon = nullptr; + inline static Ref MeshFileIcon = nullptr; + inline static Ref StaticMeshFileIcon = nullptr; + inline static Ref MeshColliderFileIcon = nullptr; + inline static Ref PhysicsMaterialFileIcon = nullptr; + inline static Ref SkeletonFileIcon = nullptr; + inline static Ref SoundConfigFileIcon = nullptr; + inline static Ref SoundGraphFileIcon = nullptr; + + // Node Graph Editor + inline static Ref CompileIcon = nullptr; + inline static Ref PinValueConnectIcon = nullptr; + inline static Ref PinValueDisconnectIcon = nullptr; + inline static Ref PinFlowConnectIcon = nullptr; + inline static Ref PinFlowDisconnectIcon = nullptr; + inline static Ref PinAudioConnectIcon = nullptr; + inline static Ref PinAudioDisconnectIcon = nullptr; + + // Textures + inline static Ref HazelLogoTexture = nullptr; + inline static Ref CheckerboardTexture = nullptr; + inline static Ref ShadowTexture = nullptr; + inline static Ref TranslucentTexture = nullptr; + inline static Ref TranslucentGradientTexture = nullptr; + + static void Init() + { + TextureSpecification spec; + spec.SamplerWrap = TextureWrap::Clamp; + + // Generic (dont forget to .Reset() these in EditorResources::Shutdown()) + GearIcon = LoadTexture("Generic/Gear.png", "GearIcon", spec); + PlusIcon = LoadTexture("Generic/Plus.png", "PlusIcon", spec); + PencilIcon = LoadTexture("Generic/Pencil.png", "PencilIcon", spec); + ForwardIcon = LoadTexture("Generic/Forward.png", "ForwardIcon", spec); + BackIcon = LoadTexture("Generic/Back.png", "BackIcon", spec); + PointerIcon = LoadTexture("Generic/Pointer.png", "PointerIcon", spec); + SearchIcon = LoadTexture("Generic/Search.png", "SearchIcon", spec); + ClearIcon = LoadTexture("Generic/Clear.png", "ClearIcon", spec); + SaveIcon = LoadTexture("Generic/Save.png", "ClearIcon", spec); + ReticuleIcon = LoadTexture("Generic/Reticule.png", "Reticule", spec); + LinkedIcon = LoadTexture("Generic/Linked.png", "LinkedIcon", spec); + UnlinkedIcon = LoadTexture("Generic/Unlinked.png", "UnlinkedIcon", spec); + + // Icons (dont forget to .Reset() these in EditorResources::Shutdown()) + AnimationIcon = LoadTexture("Icons/Animation.png", "AnimationIcon", spec); + AssetIcon = LoadTexture("Icons/Generic.png", "GenericIcon", spec); + AudioIcon = LoadTexture("Icons/Audio.png", "AudioIcon", spec); + AudioListenerIcon = LoadTexture("Icons/AudioListener.png", "AudioListenerIcon", spec); + BoxColliderIcon = LoadTexture("Icons/BoxCollider.png", "BoxColliderIcon", spec); + BoxCollider2DIcon = LoadTexture("Icons/BoxCollider2D.png", "BoxCollider2DIcon", spec); + CameraIcon = LoadTexture("Icons/Camera.png", "CameraIcon", spec); + CapsuleColliderIcon = LoadTexture("Icons/CapsuleCollider.png", "CapsuleColliderIcon", spec); + CharacterControllerIcon = LoadTexture("Icons/CharacterController.png", "CharacterControllerIcon", spec); + CircleCollider2DIcon = LoadTexture("Icons/CircleCollider2D.png", "CircleCollider2DIcon", spec); + DirectionalLightIcon = LoadTexture("Icons/DirectionalLight.png", "DirectionalLightIcon", spec); + FixedJointIcon = LoadTexture("Icons/FixedJoint.png", "FixedJointIcon", spec); + MeshIcon = LoadTexture("Icons/Mesh.png", "MeshIcon", spec); + MeshColliderIcon = LoadTexture("Icons/MeshCollider.png", "MeshColliderIcon", spec); + PointLightIcon = LoadTexture("Icons/PointLight.png", "PointLightIcon", spec); + RigidBodyIcon = LoadTexture("Icons/RigidBody.png", "RigidBodyIcon", spec); + RigidBody2DIcon = LoadTexture("Icons/RigidBody2D.png", "RigidBody2DIcon", spec); + ScriptIcon = LoadTexture("Icons/Script.png", "ScriptIcon", spec); + SkeletonIcon = LoadTexture("Icons/Skeleton.png", "SkeletonIcon", spec); + SpriteIcon = LoadTexture("Icons/SpriteRenderer.png", "SpriteIcon", spec); + SkyLightIcon = LoadTexture("Icons/SkyLight.png", "SkyLightIcon", spec); + CompoundColliderIcon = LoadTexture("Icons/CompoundCollider.png", "CompoundColliderIcon", spec); + SphereColliderIcon = LoadTexture("Icons/SphereCollider.png", "SphereColliderIcon", spec); + SpotLightIcon = LoadTexture("Icons/SpotLight.png", "SpotLightIcon", spec); + StaticMeshIcon = LoadTexture("Icons/StaticMesh.png", "StaticMeshIcon", spec); + TextIcon = LoadTexture("Icons/Text.png", "TextIcon", spec); + TransformIcon = LoadTexture("Icons/Transform.png", "TransformIcon", spec); + RefreshIcon = LoadTexture("Viewport/RotateTool.png", "RefreshIcon", spec); + + // Viewport (dont forget to .Reset() these in EditorResources::Shutdown()) + PlayIcon = LoadTexture("Viewport/Play.png", "PlayIcon", spec); + PauseIcon = LoadTexture("Viewport/Pause.png", "PauseIcon", spec); + StopIcon = LoadTexture("Viewport/Stop.png", "StopIcon", spec); + SimulateIcon = LoadTexture("Viewport/Simulate.png", "SimulateIcon", spec); + MoveIcon = LoadTexture("Viewport/MoveTool.png", "MoveIcon", spec); + RotateIcon = LoadTexture("Viewport/RotateTool.png", "RotateIcon", spec); + ScaleIcon = LoadTexture("Viewport/ScaleTool.png", "ScaleIcon", spec); + + // Window (dont forget to .Reset() these in EditorResources::Shutdown()) + MinimizeIcon = LoadTexture("Window/Minimize.png", "MinimizeIcon", spec); + MaximizeIcon = LoadTexture("Window/Maximize.png", "MaximizeIcon", spec); + RestoreIcon = LoadTexture("Window/Restore.png", "RestoreIcon", spec); + CloseIcon = LoadTexture("Window/Close.png", "CloseIcon", spec); + + // Content Browser (dont forget to .Reset() these in EditorResources::Shutdown()) + FolderIcon = LoadTexture("ContentBrowser/Folder.png", "FolderIcon", spec); + FileIcon = LoadTexture("ContentBrowser/File.png", "FileIcon", spec); + FBXFileIcon = LoadTexture("ContentBrowser/FBX.png", "FBXFileIcon", spec); + OBJFileIcon = LoadTexture("ContentBrowser/OBJ.png", "OBJFileIcon", spec); + GLTFFileIcon = LoadTexture("ContentBrowser/GLTF.png", "GLTFFileIcon", spec); + GLBFileIcon = LoadTexture("ContentBrowser/GLB.png", "GLBFileIcon", spec); + WAVFileIcon = LoadTexture("ContentBrowser/WAV.png", "WAVFileIcon", spec); + MP3FileIcon = LoadTexture("ContentBrowser/MP3.png", "MP3FileIcon", spec); + OGGFileIcon = LoadTexture("ContentBrowser/OGG.png", "OGGFileIcon", spec); + CSFileIcon = LoadTexture("ContentBrowser/CS.png", "CSFileIcon", spec); + PNGFileIcon = LoadTexture("ContentBrowser/PNG.png", "PNGFileIcon", spec); + JPGFileIcon = LoadTexture("ContentBrowser/JPG.png", "JPGFileIcon", spec); + MaterialFileIcon = LoadTexture("ContentBrowser/Material.png", "MaterialFileIcon", spec); + SceneFileIcon = LoadTexture("Hazel-IconLogo-2023.png", "SceneFileIcon", spec); + PrefabFileIcon = LoadTexture("ContentBrowser/Prefab.png", "PrefabFileIcon", spec); + FontFileIcon = LoadTexture("ContentBrowser/Font.png", "FontFileIcon", spec); + AnimationFileIcon = LoadTexture("ContentBrowser/Animation.png", "AnimationFileIcon", spec); + AnimationGraphFileIcon = LoadTexture("ContentBrowser/AnimationGraph.png", "AnimationGraphFileIcon", spec); + MeshFileIcon = LoadTexture("ContentBrowser/Mesh.png", "MeshFileIcon", spec); + StaticMeshFileIcon = LoadTexture("ContentBrowser/StaticMesh.png", "StaticMeshFileIcon", spec); + MeshColliderFileIcon = LoadTexture("ContentBrowser/MeshCollider.png", "MeshColliderFileIcon", spec); + PhysicsMaterialFileIcon = LoadTexture("ContentBrowser/PhysicsMaterial.png", "PhysicsMaterialFileIcon", spec); + SkeletonFileIcon = LoadTexture("ContentBrowser/Skeleton.png", "SkeletonFileIcon", spec); + SoundConfigFileIcon = LoadTexture("ContentBrowser/SoundConfig.png", "SoundConfigFileIcon", spec); + SoundGraphFileIcon = LoadTexture("ContentBrowser/SoundGraph.png", "SoundGraphFileIcon", spec); + + // Node Graph (dont forget to .Reset() these in EditorResources::Shutdown()) + CompileIcon = LoadTexture("NodeGraph/Compile.png", "Compile", spec); + PinValueConnectIcon = LoadTexture("NodeGraph/Pins/ValueConnect.png", "ValueConnect", spec); + PinValueDisconnectIcon = LoadTexture("NodeGraph/Pins/ValueDisconnect.png", "ValueDisconnect", spec); + PinFlowConnectIcon = LoadTexture("NodeGraph/Pins/FlowConnect.png", "FlowConnect", spec); + PinFlowDisconnectIcon = LoadTexture("NodeGraph/Pins/FlowDisconnect.png", "FlowDisconnect", spec); + PinAudioConnectIcon = LoadTexture("NodeGraph/Pins/AudioConnect.png", "AudioConnect", spec); + PinAudioDisconnectIcon = LoadTexture("NodeGraph/Pins/AudioDisconnect.png", "AudioDisconnect", spec); + + // Textures (dont forget to .Reset() these in EditorResources::Shutdown()) + HazelLogoTexture = LoadTexture("HazelLogo_Light.png"); + CheckerboardTexture = LoadTexture("Checkerboard.tga"); + ShadowTexture = LoadTexture("Panels/Shadow.png", "ShadowTexture", spec); + TranslucentTexture = LoadTexture("Panels/Translucent.png", "TranslucentTexture", spec); + TranslucentGradientTexture = LoadTexture("Panels/TranslucentGradient.png", "TranslucentGradientTexture", spec); + } + + static void Shutdown() + { + // Generic + GearIcon.Reset(); + PlusIcon.Reset(); + PencilIcon.Reset(); + ForwardIcon.Reset(); + BackIcon.Reset(); + PointerIcon.Reset(); + SearchIcon.Reset(); + ClearIcon.Reset(); + SaveIcon.Reset(); + ReticuleIcon.Reset(); + LinkedIcon.Reset(); + UnlinkedIcon.Reset(); + + // Icons + AnimationIcon.Reset(); + AssetIcon.Reset(); + AudioIcon.Reset(); + AudioListenerIcon.Reset(); + BoxColliderIcon.Reset(); + BoxCollider2DIcon.Reset(); + CameraIcon.Reset(); + CapsuleColliderIcon.Reset(); + CharacterControllerIcon.Reset(); + CircleCollider2DIcon.Reset(); + DirectionalLightIcon.Reset(); + FixedJointIcon.Reset(); + MeshIcon.Reset(); + MeshColliderIcon.Reset(); + PointLightIcon.Reset(); + RigidBodyIcon.Reset(); + RigidBody2DIcon.Reset(); + ScriptIcon.Reset(); + SkeletonIcon.Reset(); + SpriteIcon.Reset(); + SkyLightIcon.Reset(); + CompoundColliderIcon.Reset(); + SphereColliderIcon.Reset(); + SpotLightIcon.Reset(); + StaticMeshIcon.Reset(); + TextIcon.Reset(); + TransformIcon.Reset(); + RefreshIcon.Reset(); + + // Viewport + PlayIcon.Reset(); + PauseIcon.Reset(); + StopIcon.Reset(); + SimulateIcon.Reset(); + MoveIcon.Reset(); + RotateIcon.Reset(); + ScaleIcon.Reset(); + + // Window + MinimizeIcon.Reset(); + MaximizeIcon.Reset(); + RestoreIcon.Reset(); + CloseIcon.Reset(); + + // Content Browser + FolderIcon.Reset(); + FileIcon.Reset(); + FBXFileIcon.Reset(); + OBJFileIcon.Reset(); + GLTFFileIcon.Reset(); + GLBFileIcon.Reset(); + WAVFileIcon.Reset(); + MP3FileIcon.Reset(); + OGGFileIcon.Reset(); + CSFileIcon.Reset(); + PNGFileIcon.Reset(); + JPGFileIcon.Reset(); + MaterialFileIcon.Reset(); + SceneFileIcon.Reset(); + PrefabFileIcon.Reset(); + FontFileIcon.Reset(); + AnimationFileIcon.Reset(); + AnimationGraphFileIcon.Reset(); + MeshFileIcon.Reset(); + StaticMeshFileIcon.Reset(); + MeshColliderFileIcon.Reset(); + PhysicsMaterialFileIcon.Reset(); + SkeletonFileIcon.Reset(); + SoundConfigFileIcon.Reset(); + SoundGraphFileIcon.Reset(); + + // Node Graph + CompileIcon.Reset(); + PinValueConnectIcon.Reset(); + PinValueDisconnectIcon.Reset(); + PinFlowConnectIcon.Reset(); + PinFlowDisconnectIcon.Reset(); + PinAudioConnectIcon.Reset(); + PinAudioDisconnectIcon.Reset(); + + // Textures + HazelLogoTexture.Reset(); + CheckerboardTexture.Reset(); + ShadowTexture.Reset(); + TranslucentTexture.Reset(); + TranslucentGradientTexture.Reset(); + } + + private: + static Ref LoadTexture(const std::filesystem::path& relativePath, TextureSpecification specification = TextureSpecification()) + { + return LoadTexture(relativePath, "", specification); + } + + static Ref LoadTexture(const std::filesystem::path& relativePath, const std::string& name, TextureSpecification specification) + { + specification.DebugName = name; + + std::filesystem::path path = std::filesystem::path("Resources") / "Editor" / relativePath; + + if (!FileSystem::Exists(path)) + { + SE_CORE_FATAL("Failed to load icon {0}! The file doesn't exist.", path.string()); + SE_CORE_VERIFY(false); + return nullptr; + } + + return Texture2D::Create(specification, path); + } + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/FontAwesome.h b/StarEngine/src/StarEngine/Editor/FontAwesome.h new file mode 100644 index 00000000..460df269 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/FontAwesome.h @@ -0,0 +1,715 @@ +#pragma once + +#define SE_FONT_ICON_FILE_NAME_FA (const char*)u8"fontawesome-webfont.ttf" + +#define SE_ICON_MIN 0xf000 +#define SE_ICON_MAX 0xf307 +#define SE_ICON_GLASS (const char*)u8"\uf000" +#define SE_ICON_MUSIC (const char*)u8"\uf001" +#define SE_ICON_SEARCH (const char*)u8"\uf002" +#define SE_ICON_ENVELOPE_O (const char*)u8"\uf003" +#define SE_ICON_HEART (const char*)u8"\uf004" +#define SE_ICON_STAR (const char*)u8"\uf005" +#define SE_ICON_STAR_O (const char*)u8"\uf006" +#define SE_ICON_USER (const char*)u8"\uf007" +#define SE_ICON_FILM (const char*)u8"\uf008" +#define SE_ICON_TH_LARGE (const char*)u8"\uf009" +#define SE_ICON_TH (const char*)u8"\uf00a" +#define SE_ICON_TH_LIST (const char*)u8"\uf00b" +#define SE_ICON_CHECK (const char*)u8"\uf00c" +#define SE_ICON_TIMES (const char*)u8"\uf00d" +#define SE_ICON_SEARCH_PLUS (const char*)u8"\uf00e" +#define SE_ICON_SEARCH_MINUS (const char*)u8"\uf010" +#define SE_ICON_POWER_OFF (const char*)u8"\uf011" +#define SE_ICON_SIGNAL (const char*)u8"\uf012" +#define SE_ICON_COG (const char*)u8"\uf013" +#define SE_ICON_TRASH_O (const char*)u8"\uf014" +#define SE_ICON_HOME (const char*)u8"\uf015" +#define SE_ICON_FILE_O (const char*)u8"\uf016" +#define SE_ICON_CLOCK_O (const char*)u8"\uf017" +#define SE_ICON_ROAD (const char*)u8"\uf018" +#define SE_ICON_DOWNLOAD (const char*)u8"\uf019" +#define SE_ICON_ARROW_CIRCLE_O_DOWN (const char*)u8"\uf01a" +#define SE_ICON_ARROW_CIRCLE_O_UP (const char*)u8"\uf01b" +#define SE_ICON_INBOX (const char*)u8"\uf01c" +#define SE_ICON_PLAY_CIRCLE_O (const char*)u8"\uf01d" +#define SE_ICON_REPEAT (const char*)u8"\uf01e" +#define SE_ICON_REFRESH (const char*)u8"\uf021" +#define SE_ICON_LIST_ALT (const char*)u8"\uf022" +#define SE_ICON_LOCK (const char*)u8"\uf023" +#define SE_ICON_FLAG (const char*)u8"\uf024" +#define SE_ICON_HEADPHONES (const char*)u8"\uf025" +#define SE_ICON_VOLUME_OFF (const char*)u8"\uf026" +#define SE_ICON_VOLUME_DOWN (const char*)u8"\uf027" +#define SE_ICON_VOLUME_UP (const char*)u8"\uf028" +#define SE_ICON_QRCODE (const char*)u8"\uf029" +#define SE_ICON_BARCODE (const char*)u8"\uf02a" +#define SE_ICON_TAG (const char*)u8"\uf02b" +#define SE_ICON_TAGS (const char*)u8"\uf02c" +#define SE_ICON_BOOK (const char*)u8"\uf02d" +#define SE_ICON_BOOKMARK (const char*)u8"\uf02e" +#define SE_ICON_PRINT (const char*)u8"\uf02f" +#define SE_ICON_CAMERA (const char*)u8"\uf030" +#define SE_ICON_FONT (const char*)u8"\uf031" +#define SE_ICON_BOLD (const char*)u8"\uf032" +#define SE_ICON_ITALIC (const char*)u8"\uf033" +#define SE_ICON_TEXT_HEIGHT (const char*)u8"\uf034" +#define SE_ICON_TEXT_WIDTH (const char*)u8"\uf035" +#define SE_ICON_ALIGN_LEFT (const char*)u8"\uf036" +#define SE_ICON_ALIGN_CENTER (const char*)u8"\uf037" +#define SE_ICON_ALIGN_RIGHT (const char*)u8"\uf038" +#define SE_ICON_ALIGN_JUSTIFY (const char*)u8"\uf039" +#define SE_ICON_LIST (const char*)u8"\uf03a" +#define SE_ICON_OUTDENT (const char*)u8"\uf03b" +#define SE_ICON_INDENT (const char*)u8"\uf03c" +#define SE_ICON_VIDEO_CAMERA (const char*)u8"\uf03d" +#define SE_ICON_PICTURE_O (const char*)u8"\uf03e" +#define SE_ICON_PENCIL (const char*)u8"\uf040" +#define SE_ICON_MAP_MARKER (const char*)u8"\uf041" +#define SE_ICON_ADJUST (const char*)u8"\uf042" +#define SE_ICON_TINT (const char*)u8"\uf043" +#define SE_ICON_PENCIL_SQUARE_O (const char*)u8"\uf044" +#define SE_ICON_SHARE_SQUARE_O (const char*)u8"\uf045" +#define SE_ICON_CHECK_SQUARE_O (const char*)u8"\uf046" +#define SE_ICON_ARROWS (const char*)u8"\uf047" +#define SE_ICON_STEP_BACKWARD (const char*)u8"\uf048" +#define SE_ICON_FAST_BACKWARD (const char*)u8"\uf049" +#define SE_ICON_BACKWARD (const char*)u8"\uf04a" +#define SE_ICON_PLAY (const char*)u8"\uf04b" +#define SE_ICON_PAUSE (const char*)u8"\uf04c" +#define SE_ICON_STOP (const char*)u8"\uf04d" +#define SE_ICON_FORWARD (const char*)u8"\uf04e" +#define SE_ICON_FAST_FORWARD (const char*)u8"\uf050" +#define SE_ICON_STEP_FORWARD (const char*)u8"\uf051" +#define SE_ICON_EJECT (const char*)u8"\uf052" +#define SE_ICON_CHEVRON_LEFT (const char*)u8"\uf053" +#define SE_ICON_CHEVRON_RIGHT (const char*)u8"\uf054" +#define SE_ICON_PLUS_CIRCLE (const char*)u8"\uf055" +#define SE_ICON_MINUS_CIRCLE (const char*)u8"\uf056" +#define SE_ICON_TIMES_CIRCLE (const char*)u8"\uf057" +#define SE_ICON_CHECK_CIRCLE (const char*)u8"\uf058" +#define SE_ICON_QUESTION_CIRCLE (const char*)u8"\uf059" +#define SE_ICON_INFO_CIRCLE (const char*)u8"\uf05a" +#define SE_ICON_CROSSHAIRS (const char*)u8"\uf05b" +#define SE_ICON_TIMES_CIRCLE_O (const char*)u8"\uf05c" +#define SE_ICON_CHECK_CIRCLE_O (const char*)u8"\uf05d" +#define SE_ICON_BAN (const char*)u8"\uf05e" +#define SE_ICON_ARROW_LEFT (const char*)u8"\uf060" +#define SE_ICON_ARROW_RIGHT (const char*)u8"\uf061" +#define SE_ICON_ARROW_UP (const char*)u8"\uf062" +#define SE_ICON_ARROW_DOWN (const char*)u8"\uf063" +#define SE_ICON_SHARE (const char*)u8"\uf064" +#define SE_ICON_EXPAND (const char*)u8"\uf065" +#define SE_ICON_COMPRESS (const char*)u8"\uf066" +#define SE_ICON_PLUS (const char*)u8"\uf067" +#define SE_ICON_MINUS (const char*)u8"\uf068" +#define SE_ICON_ASTERISK (const char*)u8"\uf069" +#define SE_ICON_EXCLAMATION_CIRCLE (const char*)u8"\uf06a" +#define SE_ICON_GIFT (const char*)u8"\uf06b" +#define SE_ICON_LEAF (const char*)u8"\uf06c" +#define SE_ICON_FIRE (const char*)u8"\uf06d" +#define SE_ICON_EYE (const char*)u8"\uf06e" +#define SE_ICON_EYE_SLASH (const char*)u8"\uf070" +#define SE_ICON_EXCLAMATION_TRIANGLE (const char*)u8"\uf071" +#define SE_ICON_PLANE (const char*)u8"\uf072" +#define SE_ICON_CALENDAR (const char*)u8"\uf073" +#define SE_ICON_RANDOM (const char*)u8"\uf074" +#define SE_ICON_COMMENT (const char*)u8"\uf075" +#define SE_ICON_MAGNET (const char*)u8"\uf076" +#define SE_ICON_CHEVRON_UP (const char*)u8"\uf077" +#define SE_ICON_CHEVRON_DOWN (const char*)u8"\uf078" +#define SE_ICON_RETWEET (const char*)u8"\uf079" +#define SE_ICON_SHOPPING_CART (const char*)u8"\uf07a" +#define SE_ICON_FOLDER (const char*)u8"\uf07b" +#define SE_ICON_FOLDER_OPEN (const char*)u8"\uf07c" +#define SE_ICON_ARROWS_V (const char*)u8"\uf07d" +#define SE_ICON_ARROWS_H (const char*)u8"\uf07e" +#define SE_ICON_BAR_CHART (const char*)u8"\uf080" +#define SE_ICON_TWITTER_SQUARE (const char*)u8"\uf081" +#define SE_ICON_FACEBOOK_SQUARE (const char*)u8"\uf082" +#define SE_ICON_CAMERA_RETRO (const char*)u8"\uf083" +#define SE_ICON_KEY (const char*)u8"\uf084" +#define SE_ICON_COGS (const char*)u8"\uf085" +#define SE_ICON_COMMENTS (const char*)u8"\uf086" +#define SE_ICON_THUMBS_O_UP (const char*)u8"\uf087" +#define SE_ICON_THUMBS_O_DOWN (const char*)u8"\uf088" +#define SE_ICON_STAR_HALF (const char*)u8"\uf089" +#define SE_ICON_HEART_O (const char*)u8"\uf08a" +#define SE_ICON_SIGN_OUT (const char*)u8"\uf08b" +#define SE_ICON_LINKEDIN_SQUARE (const char*)u8"\uf08c" +#define SE_ICON_THUMB_TACK (const char*)u8"\uf08d" +#define SE_ICON_EXTERNAL_LINK (const char*)u8"\uf08e" +#define SE_ICON_SIGN_IN (const char*)u8"\uf090" +#define SE_ICON_TROPHY (const char*)u8"\uf091" +#define SE_ICON_GITHUB_SQUARE (const char*)u8"\uf092" +#define SE_ICON_UPLOAD (const char*)u8"\uf093" +#define SE_ICON_LEMON_O (const char*)u8"\uf094" +#define SE_ICON_PHONE (const char*)u8"\uf095" +#define SE_ICON_SQUARE_O (const char*)u8"\uf096" +#define SE_ICON_BOOKMARK_O (const char*)u8"\uf097" +#define SE_ICON_PHONE_SQUARE (const char*)u8"\uf098" +#define SE_ICON_TWITTER (const char*)u8"\uf099" +#define SE_ICON_FACEBOOK (const char*)u8"\uf09a" +#define SE_ICON_GITHUB (const char*)u8"\uf09b" +#define SE_ICON_UNLOCK (const char*)u8"\uf09c" +#define SE_ICON_CREDIT_CARD (const char*)u8"\uf09d" +#define SE_ICON_RSS (const char*)u8"\uf09e" +#define SE_ICON_HDD_O (const char*)u8"\uf0a0" +#define SE_ICON_BULLHORN (const char*)u8"\uf0a1" +#define SE_ICON_BELL (const char*)u8"\uf0f3" +#define SE_ICON_CERTIFICATE (const char*)u8"\uf0a3" +#define SE_ICON_HAND_O_RIGHT (const char*)u8"\uf0a4" +#define SE_ICON_HAND_O_LEFT (const char*)u8"\uf0a5" +#define SE_ICON_HAND_O_UP (const char*)u8"\uf0a6" +#define SE_ICON_HAND_O_DOWN (const char*)u8"\uf0a7" +#define SE_ICON_ARROW_CIRCLE_LEFT (const char*)u8"\uf0a8" +#define SE_ICON_ARROW_CIRCLE_RIGHT (const char*)u8"\uf0a9" +#define SE_ICON_ARROW_CIRCLE_UP (const char*)u8"\uf0aa" +#define SE_ICON_ARROW_CIRCLE_DOWN (const char*)u8"\uf0ab" +#define SE_ICON_GLOBE (const char*)u8"\uf0ac" +#define SE_ICON_GLOBE_E (const char*)u8"\uf304" +#define SE_ICON_GLOBE_W (const char*)u8"\uf305" +#define SE_ICON_WRENCH (const char*)u8"\uf0ad" +#define SE_ICON_TASKS (const char*)u8"\uf0ae" +#define SE_ICON_FILTER (const char*)u8"\uf0b0" +#define SE_ICON_BRIEFCASE (const char*)u8"\uf0b1" +#define SE_ICON_ARROWS_ALT (const char*)u8"\uf0b2" +#define SE_ICON_USERS (const char*)u8"\uf0c0" +#define SE_ICON_LINK (const char*)u8"\uf0c1" +#define SE_ICON_CLOUD (const char*)u8"\uf0c2" +#define SE_ICON_FLASK (const char*)u8"\uf0c3" +#define SE_ICON_SCISSORS (const char*)u8"\uf0c4" +#define SE_ICON_FILES_O (const char*)u8"\uf0c5" +#define SE_ICON_PAPERCLIP (const char*)u8"\uf0c6" +#define SE_ICON_FLOPPY_O (const char*)u8"\uf0c7" +#define SE_ICON_SQUARE (const char*)u8"\uf0c8" +#define SE_ICON_BARS (const char*)u8"\uf0c9" +#define SE_ICON_LIST_UL (const char*)u8"\uf0ca" +#define SE_ICON_LIST_OL (const char*)u8"\uf0cb" +#define SE_ICON_STRIKETHROUGH (const char*)u8"\uf0cc" +#define SE_ICON_UNDERLINE (const char*)u8"\uf0cd" +#define SE_ICON_TABLE (const char*)u8"\uf0ce" +#define SE_ICON_MAGIC (const char*)u8"\uf0d0" +#define SE_ICON_TRUCK (const char*)u8"\uf0d1" +#define SE_ICON_PINTEREST (const char*)u8"\uf0d2" +#define SE_ICON_PINTEREST_SQUARE (const char*)u8"\uf0d3" +#define SE_ICON_GOOGLE_PLUS_SQUARE (const char*)u8"\uf0d4" +#define SE_ICON_GOOGLE_PLUS (const char*)u8"\uf0d5" +#define SE_ICON_MONEY (const char*)u8"\uf0d6" +#define SE_ICON_CARET_DOWN (const char*)u8"\uf0d7" +#define SE_ICON_CARET_UP (const char*)u8"\uf0d8" +#define SE_ICON_CARET_LEFT (const char*)u8"\uf0d9" +#define SE_ICON_CARET_RIGHT (const char*)u8"\uf0da" +#define SE_ICON_COLUMNS (const char*)u8"\uf0db" +#define SE_ICON_SORT (const char*)u8"\uf0dc" +#define SE_ICON_SORT_DESC (const char*)u8"\uf0dd" +#define SE_ICON_SORT_ASC (const char*)u8"\uf0de" +#define SE_ICON_ENVELOPE (const char*)u8"\uf0e0" +#define SE_ICON_LINKEDIN (const char*)u8"\uf0e1" +#define SE_ICON_UNDO (const char*)u8"\uf0e2" +#define SE_ICON_GAVEL (const char*)u8"\uf0e3" +#define SE_ICON_TACHOMETER (const char*)u8"\uf0e4" +#define SE_ICON_COMMENT_O (const char*)u8"\uf0e5" +#define SE_ICON_COMMENTS_O (const char*)u8"\uf0e6" +#define SE_ICON_BOLT (const char*)u8"\uf0e7" +#define SE_ICON_SITEMAP (const char*)u8"\uf0e8" +#define SE_ICON_UMBRELLA (const char*)u8"\uf0e9" +#define SE_ICON_CLIPBOARD (const char*)u8"\uf0ea" +#define SE_ICON_LIGHTBULB_O (const char*)u8"\uf0eb" +#define SE_ICON_EXCHANGE (const char*)u8"\uf0ec" +#define SE_ICON_CLOUD_DOWNLOAD (const char*)u8"\uf0ed" +#define SE_ICON_CLOUD_UPLOAD (const char*)u8"\uf0ee" +#define SE_ICON_USER_MD (const char*)u8"\uf0f0" +#define SE_ICON_STETHOSCOPE (const char*)u8"\uf0f1" +#define SE_ICON_SUITCASE (const char*)u8"\uf0f2" +#define SE_ICON_BELL_O (const char*)u8"\uf0a2" +#define SE_ICON_COFFEE (const char*)u8"\uf0f4" +#define SE_ICON_CUTLERY (const char*)u8"\uf0f5" +#define SE_ICON_FILE_TEXT_O (const char*)u8"\uf0f6" +#define SE_ICON_BUILDING_O (const char*)u8"\uf0f7" +#define SE_ICON_HOSPITAL_O (const char*)u8"\uf0f8" +#define SE_ICON_AMBULANCE (const char*)u8"\uf0f9" +#define SE_ICON_MEDKIT (const char*)u8"\uf0fa" +#define SE_ICON_FIGHTER_JET (const char*)u8"\uf0fb" +#define SE_ICON_BEER (const char*)u8"\uf0fc" +#define SE_ICON_H_SQUARE (const char*)u8"\uf0fd" +#define SE_ICON_PLUS_SQUARE (const char*)u8"\uf0fe" +#define SE_ICON_ANGLE_DOUBLE_LEFT (const char*)u8"\uf100" +#define SE_ICON_ANGLE_DOUBLE_RIGHT (const char*)u8"\uf101" +#define SE_ICON_ANGLE_DOUBLE_UP (const char*)u8"\uf102" +#define SE_ICON_ANGLE_DOUBLE_DOWN (const char*)u8"\uf103" +#define SE_ICON_ANGLE_LEFT (const char*)u8"\uf104" +#define SE_ICON_ANGLE_RIGHT (const char*)u8"\uf105" +#define SE_ICON_ANGLE_UP (const char*)u8"\uf106" +#define SE_ICON_ANGLE_DOWN (const char*)u8"\uf107" +#define SE_ICON_DESKTOP (const char*)u8"\uf108" +#define SE_ICON_LAPTOP (const char*)u8"\uf109" +#define SE_ICON_TABLET (const char*)u8"\uf10a" +#define SE_ICON_MOBILE (const char*)u8"\uf10b" +#define SE_ICON_CIRCLE_O (const char*)u8"\uf10c" +#define SE_ICON_QUOTE_LEFT (const char*)u8"\uf10d" +#define SE_ICON_QUOTE_RIGHT (const char*)u8"\uf10e" +#define SE_ICON_SPINNER (const char*)u8"\uf110" +#define SE_ICON_CIRCLE (const char*)u8"\uf111" +#define SE_ICON_REPLY (const char*)u8"\uf112" +#define SE_ICON_GITHUB_ALT (const char*)u8"\uf113" +#define SE_ICON_FOLDER_O (const char*)u8"\uf114" +#define SE_ICON_FOLDER_OPEN_O (const char*)u8"\uf115" +#define SE_ICON_SMILE_O (const char*)u8"\uf118" +#define SE_ICON_FROWN_O (const char*)u8"\uf119" +#define SE_ICON_MEH_O (const char*)u8"\uf11a" +#define SE_ICON_GAMEPAD (const char*)u8"\uf11b" +#define SE_ICON_KEYBOARD_O (const char*)u8"\uf11c" +#define SE_ICON_FLAG_O (const char*)u8"\uf11d" +#define SE_ICON_FLAG_CHECKERED (const char*)u8"\uf11e" +#define SE_ICON_TERMINAL (const char*)u8"\uf120" +#define SE_ICON_CODE (const char*)u8"\uf121" +#define SE_ICON_REPLY_ALL (const char*)u8"\uf122" +#define SE_ICON_STAR_HALF_O (const char*)u8"\uf123" +#define SE_ICON_LOCATION_ARROW (const char*)u8"\uf124" +#define SE_ICON_CROP (const char*)u8"\uf125" +#define SE_ICON_CODE_FORK (const char*)u8"\uf126" +#define SE_ICON_CHAIN_BROKEN (const char*)u8"\uf127" +#define SE_ICON_QUESTION (const char*)u8"\uf128" +#define SE_ICON_INFO (const char*)u8"\uf129" +#define SE_ICON_EXCLAMATION (const char*)u8"\uf12a" +#define SE_ICON_SUPERSCRIPT (const char*)u8"\uf12b" +#define SE_ICON_SUBSCRIPT (const char*)u8"\uf12c" +#define SE_ICON_ERASER (const char*)u8"\uf12d" +#define SE_ICON_PUZZLE_PIECE (const char*)u8"\uf12e" +#define SE_ICON_MICROPHONE (const char*)u8"\uf130" +#define SE_ICON_MICROPHONE_SLASH (const char*)u8"\uf131" +#define SE_ICON_SHIELD (const char*)u8"\uf132" +#define SE_ICON_CALENDAR_O (const char*)u8"\uf133" +#define SE_ICON_FIRE_EXTINGUISHER (const char*)u8"\uf134" +#define SE_ICON_ROCKET (const char*)u8"\uf135" +#define SE_ICON_MAXCDN (const char*)u8"\uf136" +#define SE_ICON_CHEVRON_CIRCLE_LEFT (const char*)u8"\uf137" +#define SE_ICON_CHEVRON_CIRCLE_RIGHT (const char*)u8"\uf138" +#define SE_ICON_CHEVRON_CIRCLE_UP (const char*)u8"\uf139" +#define SE_ICON_CHEVRON_CIRCLE_DOWN (const char*)u8"\uf13a" +#define SE_ICON_HTML5 (const char*)u8"\uf13b" +#define SE_ICON_CSS3 (const char*)u8"\uf13c" +#define SE_ICON_ANCHOR (const char*)u8"\uf13d" +#define SE_ICON_UNLOCK_ALT (const char*)u8"\uf13e" +#define SE_ICON_BULLSEYE (const char*)u8"\uf140" +#define SE_ICON_ELLIPSIS_H (const char*)u8"\uf141" +#define SE_ICON_ELLIPSIS_V (const char*)u8"\uf142" +#define SE_ICON_RSS_SQUARE (const char*)u8"\uf143" +#define SE_ICON_PLAY_CIRCLE (const char*)u8"\uf144" +#define SE_ICON_TICKET (const char*)u8"\uf145" +#define SE_ICON_MINUS_SQUARE (const char*)u8"\uf146" +#define SE_ICON_MINUS_SQUARE_O (const char*)u8"\uf147" +#define SE_ICON_LEVEL_UP (const char*)u8"\uf148" +#define SE_ICON_LEVEL_DOWN (const char*)u8"\uf149" +#define SE_ICON_CHECK_SQUARE (const char*)u8"\uf14a" +#define SE_ICON_PENCIL_SQUARE (const char*)u8"\uf14b" +#define SE_ICON_EXTERNAL_LINK_SQUARE (const char*)u8"\uf14c" +#define SE_ICON_SHARE_SQUARE (const char*)u8"\uf14d" +#define SE_ICON_COMPASS (const char*)u8"\uf14e" +#define SE_ICON_CARET_SQUARE_O_DOWN (const char*)u8"\uf150" +#define SE_ICON_CARET_SQUARE_O_UP (const char*)u8"\uf151" +#define SE_ICON_CARET_SQUARE_O_RIGHT (const char*)u8"\uf152" +#define SE_ICON_EUR (const char*)u8"\uf153" +#define SE_ICON_GBP (const char*)u8"\uf154" +#define SE_ICON_USD (const char*)u8"\uf155" +#define SE_ICON_INR (const char*)u8"\uf156" +#define SE_ICON_JPY (const char*)u8"\uf157" +#define SE_ICON_RUB (const char*)u8"\uf158" +#define SE_ICON_KRW (const char*)u8"\uf159" +#define SE_ICON_BTC (const char*)u8"\uf15a" +#define SE_ICON_FILE (const char*)u8"\uf15b" +#define SE_ICON_FILE_TEXT (const char*)u8"\uf15c" +#define SE_ICON_SORT_ALPHA_ASC (const char*)u8"\uf15d" +#define SE_ICON_SORT_ALPHA_DESC (const char*)u8"\uf15e" +#define SE_ICON_SORT_AMOUNT_ASC (const char*)u8"\uf160" +#define SE_ICON_SORT_AMOUNT_DESC (const char*)u8"\uf161" +#define SE_ICON_SORT_NUMERIC_ASC (const char*)u8"\uf162" +#define SE_ICON_SORT_NUMERIC_DESC (const char*)u8"\uf163" +#define SE_ICON_THUMBS_UP (const char*)u8"\uf164" +#define SE_ICON_THUMBS_DOWN (const char*)u8"\uf165" +#define SE_ICON_YOUTUBE_SQUARE (const char*)u8"\uf166" +#define SE_ICON_YOUTUBE (const char*)u8"\uf167" +#define SE_ICON_XING (const char*)u8"\uf168" +#define SE_ICON_XING_SQUARE (const char*)u8"\uf169" +#define SE_ICON_YOUTUBE_PLAY (const char*)u8"\uf16a" +#define SE_ICON_DROPBOX (const char*)u8"\uf16b" +#define SE_ICON_STACK_OVERFLOW (const char*)u8"\uf16c" +#define SE_ICON_INSTAGRAM (const char*)u8"\uf16d" +#define SE_ICON_FLICKR (const char*)u8"\uf16e" +#define SE_ICON_ADN (const char*)u8"\uf170" +#define SE_ICON_BITBUCKET (const char*)u8"\uf171" +#define SE_ICON_BITBUCKET_SQUARE (const char*)u8"\uf172" +#define SE_ICON_TUMBLR (const char*)u8"\uf173" +#define SE_ICON_TUMBLR_SQUARE (const char*)u8"\uf174" +#define SE_ICON_LONG_ARROW_DOWN (const char*)u8"\uf175" +#define SE_ICON_LONG_ARROW_UP (const char*)u8"\uf176" +#define SE_ICON_LONG_ARROW_LEFT (const char*)u8"\uf177" +#define SE_ICON_LONG_ARROW_RIGHT (const char*)u8"\uf178" +#define SE_ICON_APPLE (const char*)u8"\uf179" +#define SE_ICON_WINDOWS (const char*)u8"\uf17a" +#define SE_ICON_ANDROID (const char*)u8"\uf17b" +#define SE_ICON_LINUX (const char*)u8"\uf17c" +#define SE_ICON_DRIBBBLE (const char*)u8"\uf17d" +#define SE_ICON_SKYPE (const char*)u8"\uf17e" +#define SE_ICON_FOURSQUARE (const char*)u8"\uf180" +#define SE_ICON_TRELLO (const char*)u8"\uf181" +#define SE_ICON_FEMALE (const char*)u8"\uf182" +#define SE_ICON_MALE (const char*)u8"\uf183" +#define SE_ICON_GRATIPAY (const char*)u8"\uf184" +#define SE_ICON_SUN_O (const char*)u8"\uf185" +#define SE_ICON_MOON_O (const char*)u8"\uf186" +#define SE_ICON_ARCHIVE (const char*)u8"\uf187" +#define SE_ICON_BUG (const char*)u8"\uf188" +#define SE_ICON_VK (const char*)u8"\uf189" +#define SE_ICON_WEIBO (const char*)u8"\uf18a" +#define SE_ICON_RENREN (const char*)u8"\uf18b" +#define SE_ICON_PAGELINES (const char*)u8"\uf18c" +#define SE_ICON_STACK_EXCHANGE (const char*)u8"\uf18d" +#define SE_ICON_ARROW_CIRCLE_O_RIGHT (const char*)u8"\uf18e" +#define SE_ICON_ARROW_CIRCLE_O_LEFT (const char*)u8"\uf190" +#define SE_ICON_CARET_SQUARE_O_LEFT (const char*)u8"\uf191" +#define SE_ICON_DOT_CIRCLE_O (const char*)u8"\uf192" +#define SE_ICON_WHEELCHAIR (const char*)u8"\uf193" +#define SE_ICON_VIMEO_SQUARE (const char*)u8"\uf194" +#define SE_ICON_TRY (const char*)u8"\uf195" +#define SE_ICON_PLUS_SQUARE_O (const char*)u8"\uf196" +#define SE_ICON_SPACE_SHUTTLE (const char*)u8"\uf197" +#define SE_ICON_SLACK (const char*)u8"\uf198" +#define SE_ICON_ENVELOPE_SQUARE (const char*)u8"\uf199" +#define SE_ICON_WORDPRESS (const char*)u8"\uf19a" +#define SE_ICON_OPENID (const char*)u8"\uf19b" +#define SE_ICON_UNIVERSITY (const char*)u8"\uf19c" +#define SE_ICON_GRADUATION_CAP (const char*)u8"\uf19d" +#define SE_ICON_YAHOO (const char*)u8"\uf19e" +#define SE_ICON_GOOGLE (const char*)u8"\uf1a0" +#define SE_ICON_REDDIT (const char*)u8"\uf1a1" +#define SE_ICON_REDDIT_SQUARE (const char*)u8"\uf1a2" +#define SE_ICON_STUMBLEUPON_CIRCLE (const char*)u8"\uf1a3" +#define SE_ICON_STUMBLEUPON (const char*)u8"\uf1a4" +#define SE_ICON_DELICIOUS (const char*)u8"\uf1a5" +#define SE_ICON_DIGG (const char*)u8"\uf1a6" +#define SE_ICON_DRUPAL (const char*)u8"\uf1a9" +#define SE_ICON_JOOMLA (const char*)u8"\uf1aa" +#define SE_ICON_LANGUAGE (const char*)u8"\uf1ab" +#define SE_ICON_FAX (const char*)u8"\uf1ac" +#define SE_ICON_BUILDING (const char*)u8"\uf1ad" +#define SE_ICON_CHILD (const char*)u8"\uf1ae" +#define SE_ICON_PAW (const char*)u8"\uf1b0" +#define SE_ICON_SPOON (const char*)u8"\uf1b1" +#define SE_ICON_CUBE (const char*)u8"\uf1b2" +#define SE_ICON_CUBES (const char*)u8"\uf1b3" +#define SE_ICON_BEHANCE (const char*)u8"\uf1b4" +#define SE_ICON_BEHANCE_SQUARE (const char*)u8"\uf1b5" +#define SE_ICON_STEAM (const char*)u8"\uf1b6" +#define SE_ICON_STEAM_SQUARE (const char*)u8"\uf1b7" +#define SE_ICON_RECYCLE (const char*)u8"\uf1b8" +#define SE_ICON_CAR (const char*)u8"\uf1b9" +#define SE_ICON_TAXI (const char*)u8"\uf1ba" +#define SE_ICON_TREE (const char*)u8"\uf1bb" +#define SE_ICON_SPOTIFY (const char*)u8"\uf1bc" +#define SE_ICON_DEVIANTART (const char*)u8"\uf1bd" +#define SE_ICON_SOUNDCLOUD (const char*)u8"\uf1be" +#define SE_ICON_DATABASE (const char*)u8"\uf1c0" +#define SE_ICON_FILE_PDF_O (const char*)u8"\uf1c1" +#define SE_ICON_FILE_WORD_O (const char*)u8"\uf1c2" +#define SE_ICON_FILE_EXCEL_O (const char*)u8"\uf1c3" +#define SE_ICON_FILE_POWERPOINT_O (const char*)u8"\uf1c4" +#define SE_ICON_FILE_IMAGE_O (const char*)u8"\uf1c5" +#define SE_ICON_FILE_ARCHIVE_O (const char*)u8"\uf1c6" +#define SE_ICON_FILE_AUDIO_O (const char*)u8"\uf1c7" +#define SE_ICON_FILE_VIDEO_O (const char*)u8"\uf1c8" +#define SE_ICON_FILE_CODE_O (const char*)u8"\uf1c9" +#define SE_ICON_VINE (const char*)u8"\uf1ca" +#define SE_ICON_CODEPEN (const char*)u8"\uf1cb" +#define SE_ICON_JSFIDDLE (const char*)u8"\uf1cc" +#define SE_ICON_LIFE_RING (const char*)u8"\uf1cd" +#define SE_ICON_CIRCLE_O_NOTCH (const char*)u8"\uf1ce" +#define SE_ICON_REBEL (const char*)u8"\uf1d0" +#define SE_ICON_EMPIRE (const char*)u8"\uf1d1" +#define SE_ICON_GIT_SQUARE (const char*)u8"\uf1d2" +#define SE_ICON_GIT (const char*)u8"\uf1d3" +#define SE_ICON_HACKER_NEWS (const char*)u8"\uf1d4" +#define SE_ICON_TENCENT_WEIBO (const char*)u8"\uf1d5" +#define SE_ICON_QQ (const char*)u8"\uf1d6" +#define SE_ICON_WEIXIN (const char*)u8"\uf1d7" +#define SE_ICON_PAPER_PLANE (const char*)u8"\uf1d8" +#define SE_ICON_PAPER_PLANE_O (const char*)u8"\uf1d9" +#define SE_ICON_HISTORY (const char*)u8"\uf1da" +#define SE_ICON_CIRCLE_THIN (const char*)u8"\uf1db" +#define SE_ICON_HEADER (const char*)u8"\uf1dc" +#define SE_ICON_PARAGRAPH (const char*)u8"\uf1dd" +#define SE_ICON_SLIDERS (const char*)u8"\uf1de" +#define SE_ICON_SHARE_ALT (const char*)u8"\uf1e0" +#define SE_ICON_SHARE_ALT_SQUARE (const char*)u8"\uf1e1" +#define SE_ICON_BOMB (const char*)u8"\uf1e2" +#define SE_ICON_FUTBOL_O (const char*)u8"\uf1e3" +#define SE_ICON_TTY (const char*)u8"\uf1e4" +#define SE_ICON_BINOCULARS (const char*)u8"\uf1e5" +#define SE_ICON_PLUG (const char*)u8"\uf1e6" +#define SE_ICON_SLIDESHARE (const char*)u8"\uf1e7" +#define SE_ICON_TWITCH (const char*)u8"\uf1e8" +#define SE_ICON_YELP (const char*)u8"\uf1e9" +#define SE_ICON_NEWSPAPER_O (const char*)u8"\uf1ea" +#define SE_ICON_WIFI (const char*)u8"\uf1eb" +#define SE_ICON_CALCULATOR (const char*)u8"\uf1ec" +#define SE_ICON_PAYPAL (const char*)u8"\uf1ed" +#define SE_ICON_GOOGLE_WALLET (const char*)u8"\uf1ee" +#define SE_ICON_CC_VISA (const char*)u8"\uf1f0" +#define SE_ICON_CC_MASTERCARD (const char*)u8"\uf1f1" +#define SE_ICON_CC_DISCOVER (const char*)u8"\uf1f2" +#define SE_ICON_CC_AMEX (const char*)u8"\uf1f3" +#define SE_ICON_CC_PAYPAL (const char*)u8"\uf1f4" +#define SE_ICON_CC_STRIPE (const char*)u8"\uf1f5" +#define SE_ICON_BELL_SLASH (const char*)u8"\uf1f6" +#define SE_ICON_BELL_SLASH_O (const char*)u8"\uf1f7" +#define SE_ICON_TRASH (const char*)u8"\uf1f8" +#define SE_ICON_COPYRIGHT (const char*)u8"\uf1f9" +#define SE_ICON_AT (const char*)u8"\uf1fa" +#define SE_ICON_EYEDROPPER (const char*)u8"\uf1fb" +#define SE_ICON_PAINT_BRUSH (const char*)u8"\uf1fc" +#define SE_ICON_BIRTHDAY_CAKE (const char*)u8"\uf1fd" +#define SE_ICON_AREA_CHART (const char*)u8"\uf1fe" +#define SE_ICON_PIE_CHART (const char*)u8"\uf200" +#define SE_ICON_LINE_CHART (const char*)u8"\uf201" +#define SE_ICON_LASTFM (const char*)u8"\uf202" +#define SE_ICON_LASTFM_SQUARE (const char*)u8"\uf203" +#define SE_ICON_TOGGLE_OFF (const char*)u8"\uf204" +#define SE_ICON_TOGGLE_ON (const char*)u8"\uf205" +#define SE_ICON_BICYCLE (const char*)u8"\uf206" +#define SE_ICON_BUS (const char*)u8"\uf207" +#define SE_ICON_IOXHOST (const char*)u8"\uf208" +#define SE_ICON_ANGELLIST (const char*)u8"\uf209" +#define SE_ICON_CC (const char*)u8"\uf20a" +#define SE_ICON_ILS (const char*)u8"\uf20b" +#define SE_ICON_MEANPATH (const char*)u8"\uf20c" +#define SE_ICON_BUYSELLADS (const char*)u8"\uf20d" +#define SE_ICON_CONNECTDEVELOP (const char*)u8"\uf20e" +#define SE_ICON_DASHCUBE (const char*)u8"\uf210" +#define SE_ICON_FORUMBEE (const char*)u8"\uf211" +#define SE_ICON_LEANPUB (const char*)u8"\uf212" +#define SE_ICON_SELLSY (const char*)u8"\uf213" +#define SE_ICON_SHIRTSINBULK (const char*)u8"\uf214" +#define SE_ICON_SIMPLYBUILT (const char*)u8"\uf215" +#define SE_ICON_SKYATLAS (const char*)u8"\uf216" +#define SE_ICON_CART_PLUS (const char*)u8"\uf217" +#define SE_ICON_CART_ARROW_DOWN (const char*)u8"\uf218" +#define SE_ICON_DIAMOND (const char*)u8"\uf219" +#define SE_ICON_SHIP (const char*)u8"\uf21a" +#define SE_ICON_USER_SECRET (const char*)u8"\uf21b" +#define SE_ICON_MOTORCYCLE (const char*)u8"\uf21c" +#define SE_ICON_STREET_VIEW (const char*)u8"\uf21d" +#define SE_ICON_HEARTBEAT (const char*)u8"\uf21e" +#define SE_ICON_VENUS (const char*)u8"\uf221" +#define SE_ICON_MARS (const char*)u8"\uf222" +#define SE_ICON_MERCURY (const char*)u8"\uf223" +#define SE_ICON_TRANSGENDER (const char*)u8"\uf224" +#define SE_ICON_TRANSGENDER_ALT (const char*)u8"\uf225" +#define SE_ICON_VENUS_DOUBLE (const char*)u8"\uf226" +#define SE_ICON_MARS_DOUBLE (const char*)u8"\uf227" +#define SE_ICON_VENUS_MARS (const char*)u8"\uf228" +#define SE_ICON_MARS_STROKE (const char*)u8"\uf229" +#define SE_ICON_MARS_STROKE_V (const char*)u8"\uf22a" +#define SE_ICON_MARS_STROKE_H (const char*)u8"\uf22b" +#define SE_ICON_NEUTER (const char*)u8"\uf22c" +#define SE_ICON_GENDERLESS (const char*)u8"\uf22d" +#define SE_ICON_FACEBOOK_OFFICIAL (const char*)u8"\uf230" +#define SE_ICON_PINTEREST_P (const char*)u8"\uf231" +#define SE_ICON_WHATSAPP (const char*)u8"\uf232" +#define SE_ICON_SERVER (const char*)u8"\uf233" +#define SE_ICON_USER_PLUS (const char*)u8"\uf234" +#define SE_ICON_USER_TIMES (const char*)u8"\uf235" +#define SE_ICON_BED (const char*)u8"\uf236" +#define SE_ICON_VIACOIN (const char*)u8"\uf237" +#define SE_ICON_TRAIN (const char*)u8"\uf238" +#define SE_ICON_SUBWAY (const char*)u8"\uf239" +#define SE_ICON_MEDIUM (const char*)u8"\uf23a" +#define SE_ICON_MEDIUM_SQUARE (const char*)u8"\uf2f8" +#define SE_ICON_Y_COMBINATOR (const char*)u8"\uf23b" +#define SE_ICON_OPTIN_MONSTER (const char*)u8"\uf23c" +#define SE_ICON_OPENCART (const char*)u8"\uf23d" +#define SE_ICON_EXPEDITEDSSL (const char*)u8"\uf23e" +#define SE_ICON_BATTERY_FULL (const char*)u8"\uf240" +#define SE_ICON_BATTERY_THREE_QUARTERS (const char*)u8"\uf241" +#define SE_ICON_BATTERY_HALF (const char*)u8"\uf242" +#define SE_ICON_BATTERY_QUARTER (const char*)u8"\uf243" +#define SE_ICON_BATTERY_EMPTY (const char*)u8"\uf244" +#define SE_ICON_MOUSE_POINTER (const char*)u8"\uf245" +#define SE_ICON_I_CURSOR (const char*)u8"\uf246" +#define SE_ICON_OBJECT_GROUP (const char*)u8"\uf247" +#define SE_ICON_OBJECT_UNGROUP (const char*)u8"\uf248" +#define SE_ICON_STICKY_NOTE (const char*)u8"\uf249" +#define SE_ICON_STICKY_NOTE_O (const char*)u8"\uf24a" +#define SE_ICON_CC_JCB (const char*)u8"\uf24b" +#define SE_ICON_CC_DINERS_CLUB (const char*)u8"\uf24c" +#define SE_ICON_CLONE (const char*)u8"\uf24d" +#define SE_ICON_BALANCE_SCALE (const char*)u8"\uf24e" +#define SE_ICON_HOURGLASS_O (const char*)u8"\uf250" +#define SE_ICON_HOURGLASS_START (const char*)u8"\uf251" +#define SE_ICON_HOURGLASS_HALF (const char*)u8"\uf252" +#define SE_ICON_HOURGLASS_END (const char*)u8"\uf253" +#define SE_ICON_HOURGLASS (const char*)u8"\uf254" +#define SE_ICON_HAND_ROCK_O (const char*)u8"\uf255" +#define SE_ICON_HAND_PAPER_O (const char*)u8"\uf256" +#define SE_ICON_HAND_SCISSORS_O (const char*)u8"\uf257" +#define SE_ICON_HAND_LIZARD_O (const char*)u8"\uf258" +#define SE_ICON_HAND_SPOCK_O (const char*)u8"\uf259" +#define SE_ICON_HAND_POINTER_O (const char*)u8"\uf25a" +#define SE_ICON_HAND_PEACE_O (const char*)u8"\uf25b" +#define SE_ICON_TRADEMARK (const char*)u8"\uf25c" +#define SE_ICON_REGISTERED (const char*)u8"\uf25d" +#define SE_ICON_CREATIVE_COMMONS (const char*)u8"\uf25e" +#define SE_ICON_GG (const char*)u8"\uf260" +#define SE_ICON_GG_CIRCLE (const char*)u8"\uf261" +#define SE_ICON_TRIPADVISOR (const char*)u8"\uf262" +#define SE_ICON_ODNOKLASSNIKI (const char*)u8"\uf263" +#define SE_ICON_ODNOKLASSNIKI_SQUARE (const char*)u8"\uf264" +#define SE_ICON_GET_POCKET (const char*)u8"\uf265" +#define SE_ICON_WIKIPEDIA_W (const char*)u8"\uf266" +#define SE_ICON_SAFARI (const char*)u8"\uf267" +#define SE_ICON_CHROME (const char*)u8"\uf268" +#define SE_ICON_FIREFOX (const char*)u8"\uf269" +#define SE_ICON_OPERA (const char*)u8"\uf26a" +#define SE_ICON_INTERNET_EXPLORER (const char*)u8"\uf26b" +#define SE_ICON_TELEVISION (const char*)u8"\uf26c" +#define SE_ICON_CONTAO (const char*)u8"\uf26d" +#define SE_ICON_500PX (const char*)u8"\uf26e" +#define SE_ICON_AMAZON (const char*)u8"\uf270" +#define SE_ICON_CALENDAR_PLUS_O (const char*)u8"\uf271" +#define SE_ICON_CALENDAR_MINUS_O (const char*)u8"\uf272" +#define SE_ICON_CALENDAR_TIMES_O (const char*)u8"\uf273" +#define SE_ICON_CALENDAR_CHECK_O (const char*)u8"\uf274" +#define SE_ICON_INDUSTRY (const char*)u8"\uf275" +#define SE_ICON_MAP_PIN (const char*)u8"\uf276" +#define SE_ICON_MAP_SIGNS (const char*)u8"\uf277" +#define SE_ICON_MAP_O (const char*)u8"\uf278" +#define SE_ICON_MAP (const char*)u8"\uf279" +#define SE_ICON_COMMENTING (const char*)u8"\uf27a" +#define SE_ICON_COMMENTING_O (const char*)u8"\uf27b" +#define SE_ICON_HOUZZ (const char*)u8"\uf27c" +#define SE_ICON_VIMEO (const char*)u8"\uf27d" +#define SE_ICON_BLACK_TIE (const char*)u8"\uf27e" +#define SE_ICON_FONTICONS (const char*)u8"\uf280" +#define SE_ICON_REDDIT_ALIEN (const char*)u8"\uf281" +#define SE_ICON_EDGE (const char*)u8"\uf282" +#define SE_ICON_CREDIT_CARD_ALT (const char*)u8"\uf283" +#define SE_ICON_CODIEPIE (const char*)u8"\uf284" +#define SE_ICON_MODX (const char*)u8"\uf285" +#define SE_ICON_FORT_AWESOME (const char*)u8"\uf286" +#define SE_ICON_USB (const char*)u8"\uf287" +#define SE_ICON_PRODUCT_HUNT (const char*)u8"\uf288" +#define SE_ICON_MIXCLOUD (const char*)u8"\uf289" +#define SE_ICON_SCRIBD (const char*)u8"\uf28a" +#define SE_ICON_PAUSE_CIRCLE (const char*)u8"\uf28b" +#define SE_ICON_PAUSE_CIRCLE_O (const char*)u8"\uf28c" +#define SE_ICON_STOP_CIRCLE (const char*)u8"\uf28d" +#define SE_ICON_STOP_CIRCLE_O (const char*)u8"\uf28e" +#define SE_ICON_SHOPPING_BAG (const char*)u8"\uf290" +#define SE_ICON_SHOPPING_BASKET (const char*)u8"\uf291" +#define SE_ICON_HASHTAG (const char*)u8"\uf292" +#define SE_ICON_BLUETOOTH (const char*)u8"\uf293" +#define SE_ICON_BLUETOOTH_B (const char*)u8"\uf294" +#define SE_ICON_PERCENT (const char*)u8"\uf295" +#define SE_ICON_GITLAB (const char*)u8"\uf296" +#define SE_ICON_WPBEGINNER (const char*)u8"\uf297" +#define SE_ICON_WPFORMS (const char*)u8"\uf298" +#define SE_ICON_ENVIRA (const char*)u8"\uf299" +#define SE_ICON_UNIVERSAL_ACCESS (const char*)u8"\uf29a" +#define SE_ICON_WHEELCHAIR_ALT (const char*)u8"\uf29b" +#define SE_ICON_QUESTION_CIRCLE_O (const char*)u8"\uf29c" +#define SE_ICON_BLIND (const char*)u8"\uf29d" +#define SE_ICON_AUDIO_DESCRIPTION (const char*)u8"\uf29e" +#define SE_ICON_VOLUME_CONTROL_PHONE (const char*)u8"\uf2a0" +#define SE_ICON_BRAILLE (const char*)u8"\uf2a1" +#define SE_ICON_ASSISTIVE_LISTENING_SYSTEMS (const char*)u8"\uf2a2" +#define SE_ICON_AMERICAN_SIGN_LANGUAGE_INTERPRETING (const char*)u8"\uf2a3" +#define SE_ICON_DEAF (const char*)u8"\uf2a4" +#define SE_ICON_GLIDE (const char*)u8"\uf2a5" +#define SE_ICON_GLIDE_G (const char*)u8"\uf2a6" +#define SE_ICON_SIGN_LANGUAGE (const char*)u8"\uf2a7" +#define SE_ICON_LOW_VISION (const char*)u8"\uf2a8" +#define SE_ICON_VIADEO (const char*)u8"\uf2a9" +#define SE_ICON_VIADEO_SQUARE (const char*)u8"\uf2aa" +#define SE_ICON_SNAPCHAT (const char*)u8"\uf2ab" +#define SE_ICON_SNAPCHAT_GHOST (const char*)u8"\uf2ac" +#define SE_ICON_SNAPCHAT_SQUARE (const char*)u8"\uf2ad" +#define SE_ICON_FIRST_ORDER (const char*)u8"\uf2b0" +#define SE_ICON_YOAST (const char*)u8"\uf2b1" +#define SE_ICON_THEMEISLE (const char*)u8"\uf2b2" +#define SE_ICON_GOOGLE_PLUS_OFFICIAL (const char*)u8"\uf2b3" +#define SE_ICON_FONT_AWESOME (const char*)u8"\uf2b4" +#define SE_ICON_HANDSHAKE_O (const char*)u8"\uf2b5" +#define SE_ICON_ENVELOPE_OPEN (const char*)u8"\uf2b6" +#define SE_ICON_ENVELOPE_OPEN_O (const char*)u8"\uf2b7" +#define SE_ICON_LINODE (const char*)u8"\uf2b8" +#define SE_ICON_ADDRESS_BOOK (const char*)u8"\uf2b9" +#define SE_ICON_ADDRESS_BOOK_O (const char*)u8"\uf2ba" +#define SE_ICON_ADDRESS_CARD (const char*)u8"\uf2bb" +#define SE_ICON_ADDRESS_CARD_O (const char*)u8"\uf2bc" +#define SE_ICON_USER_CIRCLE (const char*)u8"\uf2bd" +#define SE_ICON_USER_CIRCLE_O (const char*)u8"\uf2be" +#define SE_ICON_USER_O (const char*)u8"\uf2c0" +#define SE_ICON_ID_BADGE (const char*)u8"\uf2c1" +#define SE_ICON_ID_CARD (const char*)u8"\uf2c2" +#define SE_ICON_ID_CARD_O (const char*)u8"\uf2c3" +#define SE_ICON_QUORA (const char*)u8"\uf2c4" +#define SE_ICON_FREE_CODE_CAMP (const char*)u8"\uf2c5" +#define SE_ICON_TELEGRAM (const char*)u8"\uf2c6" +#define SE_ICON_THERMOMETER_FULL (const char*)u8"\uf2c7" +#define SE_ICON_THERMOMETER_THREE_QUARTERS (const char*)u8"\uf2c8" +#define SE_ICON_THERMOMETER_HALF (const char*)u8"\uf2c9" +#define SE_ICON_THERMOMETER_QUARTER (const char*)u8"\uf2ca" +#define SE_ICON_THERMOMETER_EMPTY (const char*)u8"\uf2cb" +#define SE_ICON_SHOWER (const char*)u8"\uf2cc" +#define SE_ICON_BATH (const char*)u8"\uf2cd" +#define SE_ICON_PODCAST (const char*)u8"\uf2ce" +#define SE_ICON_WINDOW_MAXIMIZE (const char*)u8"\uf2d0" +#define SE_ICON_WINDOW_MINIMIZE (const char*)u8"\uf2d1" +#define SE_ICON_WINDOW_RESTORE (const char*)u8"\uf2d2" +#define SE_ICON_WINDOW_CLOSE (const char*)u8"\uf2d3" +#define SE_ICON_WINDOW_CLOSE_O (const char*)u8"\uf2d4" +#define SE_ICON_BANDCAMP (const char*)u8"\uf2d5" +#define SE_ICON_GRAV (const char*)u8"\uf2d6" +#define SE_ICON_ETSY (const char*)u8"\uf2d7" +#define SE_ICON_IMDB (const char*)u8"\uf2d8" +#define SE_ICON_RAVELRY (const char*)u8"\uf2d9" +#define SE_ICON_EERCAST (const char*)u8"\uf2da" +#define SE_ICON_MICROCHIP (const char*)u8"\uf2db" +#define SE_ICON_SNOWFLAKE_O (const char*)u8"\uf2dc" +#define SE_ICON_SUPERPOWERS (const char*)u8"\uf2dd" +#define SE_ICON_WPEXPLORER (const char*)u8"\uf2de" +#define SE_ICON_MEETUP (const char*)u8"\uf2e0" +#define SE_ICON_MASTODON (const char*)u8"\uf2e1" +#define SE_ICON_MASTODON_ALT (const char*)u8"\uf2e2" +#define SE_ICON_FORK_AWESOME (const char*)u8"\uf2e3" +#define SE_ICON_PEERTUBE (const char*)u8"\uf2e4" +#define SE_ICON_DIASPORA (const char*)u8"\uf2e5" +#define SE_ICON_FRIENDICA (const char*)u8"\uf2e6" +#define SE_ICON_GNU_SOCIAL (const char*)u8"\uf2e7" +#define SE_ICON_LIBERAPAY_SQUARE (const char*)u8"\uf2e8" +#define SE_ICON_LIBERAPAY (const char*)u8"\uf2e9" +#define SE_ICON_SCUTTLEBUTT (const char*)u8"\uf2ea" +#define SE_ICON_HUBZILLA (const char*)u8"\uf2eb" +#define SE_ICON_SOCIAL_HOME (const char*)u8"\uf2ec" +#define SE_ICON_ARTSTATION (const char*)u8"\uf2ed" +#define SE_ICON_DISCORD (const char*)u8"\uf2ee" +#define SE_ICON_DISCORD_ALT (const char*)u8"\uf2ef" +#define SE_ICON_PATREON (const char*)u8"\uf2f0" +#define SE_ICON_SNOWDRIFT (const char*)u8"\uf2f1" +#define SE_ICON_ACTIVITYPUB (const char*)u8"\uf2f2" +#define SE_ICON_ETHEREUM (const char*)u8"\uf2f3" +#define SE_ICON_KEYBASE (const char*)u8"\uf2f4" +#define SE_ICON_SHAARLI (const char*)u8"\uf2f5" +#define SE_ICON_SHAARLI_O (const char*)u8"\uf2f6" +#define SE_ICON_KEY_MODERN (const char*)u8"\uf2f7" +#define SE_ICON_XMPP (const char*)u8"\uf2f9" +#define SE_ICON_ARCHIVE_ORG (const char*)u8"\uf2fc" +#define SE_ICON_FREEDOMBOX (const char*)u8"\uf2fd" +#define SE_ICON_FACEBOOK_MESSENGER (const char*)u8"\uf2fe" +#define SE_ICON_DEBIAN (const char*)u8"\uf2ff" +#define SE_ICON_MASTODON_SQUARE (const char*)u8"\uf300" +#define SE_ICON_TIPEEE (const char*)u8"\uf301" +#define SE_ICON_REACT (const char*)u8"\uf302" +#define SE_ICON_DOGMAZIC (const char*)u8"\uf303" +#define SE_ICON_NEXTCLOUD (const char*)u8"\uf306" +#define SE_ICON_NEXTCLOUD_SQUARE (const char*)u8"\uf307" diff --git a/StarEngine/src/StarEngine/Editor/MeshColliderEditor.cpp b/StarEngine/src/StarEngine/Editor/MeshColliderEditor.cpp new file mode 100644 index 00000000..758918d4 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/MeshColliderEditor.cpp @@ -0,0 +1,712 @@ +#include "sepch.h" +#include "MeshColliderEditor.h" + +#include "StarEngine/Asset/AssetImporter.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Physics/PhysicsSystem.h" +#include "StarEngine/Editor/EditorResources.h" +#include "StarEngine/Debug/Profiler.h" + +#include +#include + +namespace StarEngine { + + namespace Utils { + + static std::stack s_MinXSizes, s_MinYSizes; + static std::stack s_MaxXSizes, s_MaxYSizes; + + static void PushMinSizeX(float minSize) + { + ImGuiStyle& style = ImGui::GetStyle(); + s_MinXSizes.push(style.WindowMinSize.x); + style.WindowMinSize.x = minSize; + } + + static void PopMinSizeX() + { + ImGuiStyle& style = ImGui::GetStyle(); + style.WindowMinSize.x = s_MinXSizes.top(); + s_MinXSizes.pop(); + } + + static void PushMinSizeY(float minSize) + { + ImGuiStyle& style = ImGui::GetStyle(); + s_MinYSizes.push(style.WindowMinSize.y); + style.WindowMinSize.y = minSize; + } + + static void PopMinSizeY() + { + ImGuiStyle& style = ImGui::GetStyle(); + style.WindowMinSize.y = s_MinYSizes.top(); + s_MinYSizes.pop(); + } + + } + + MeshColliderEditor::MeshColliderEditor() + : AssetEditor("MeshColliderEditor") + { + } + + MeshColliderEditor::~MeshColliderEditor() + { + } + + void MeshColliderEditor::OnUpdate(Timestep ts) + { + if (m_SelectedTabData == nullptr) + return; + + m_SelectedTabData->ViewportCamera.SetActive(m_SelectedTabData->ViewportFocused); + m_SelectedTabData->ViewportCamera.OnUpdate(ts); + m_SelectedTabData->ViewportScene->OnRenderEditor(m_SelectedTabData->ViewportRenderer, m_SelectedTabData->ViewportCamera); + } + + void MeshColliderEditor::OnEvent(Event& e) + { + if (m_SelectedTabData == nullptr) + return; + + if (m_SelectedTabData->ViewportHovered) + m_SelectedTabData->ViewportCamera.OnEvent(e); + } + + void MeshColliderEditor::SetAsset(const Ref& asset) + { + m_WindowOpen = true; + + const auto& metadata = Project::GetEditorAssetManager()->GetMetadata(asset->Handle); + std::string name = metadata.FilePath.stem().string(); + + if (auto it = m_OpenTabs.find(asset->Handle); it != m_OpenTabs.end()) + { + m_SelectedTabData = it->second; + ImGui::SetWindowFocus(name.c_str()); + return; + } + + auto tabData = m_OpenTabs[asset->Handle] = std::make_shared(); + tabData->Name = name; + tabData->ColliderAsset = asset.As(); + + tabData->ViewportScene = Ref::Create("MeshColliderPreviewScene", true, false); + tabData->ColliderEntity = tabData->ViewportScene->CreateEntity("ColliderEntity"); + tabData->ColliderEntity.GetComponent().Scale = tabData->ColliderAsset->PreviewScale; + if (tabData->ColliderAsset->ColliderMesh != 0) + CookMeshCollider(tabData); + + UpdatePreviewEntity(tabData); + + tabData->SkyLight = tabData->ViewportScene->CreateEntity("SkyLight"); + auto& skyLight = tabData->SkyLight.AddComponent(); + skyLight.DynamicSky = true; + Ref preethamEnv = Renderer::CreatePreethamSky(skyLight.TurbidityAzimuthInclination.x, skyLight.TurbidityAzimuthInclination.y, skyLight.TurbidityAzimuthInclination.z); + skyLight.SceneEnvironment = AssetManager::AddMemoryOnlyAsset(Ref::Create(preethamEnv, preethamEnv)); + + tabData->DirectionalLight = tabData->ViewportScene->CreateEntity("DirectionalLight"); + tabData->DirectionalLight.AddComponent(); + tabData->DirectionalLight.GetComponent().SetRotationEuler(glm::radians(glm::vec3{80.0f, 10.0f, 0.0f})); + + tabData->ViewportRenderer = Ref::Create(tabData->ViewportScene); + auto& rendererOptions = tabData->ViewportRenderer->GetOptions(); + rendererOptions.PhysicsColliderMode = SceneRendererOptions::PhysicsColliderView::All; + rendererOptions.ShowPhysicsColliders = true; + + tabData->ViewportPanelName = std::format("Viewport##{}-{}", tabData->Name, "Viewport"); + tabData->SettingsPanelName = std::format("Settings##{}-{}", tabData->Name, "Settings"); + tabData->ResetDockspace = true; + + m_TabToFocus = name; + m_SelectedTabData = tabData; + } + + void MeshColliderEditor::OnImGuiRender() + { + SE_PROFILE_FUNCTION("MeshColliderEditor::OnImGuiRender"); + + if (m_OpenTabs.empty() || !m_WindowOpen) + return; + + static size_t focusFrameCounter = 0; + + const char* windowTitle = "Mesh Collider Editor"; + + // Dockspace + { + Utils::PushMinSizeY(100.0f); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_MenuBar; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin(windowTitle, &m_WindowOpen, window_flags); + ImGui::PopStyleVar(); + + // Maximize + auto window = ImGui::GetCurrentWindow(); + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + if (window->TitleBarRect().Contains(ImGui::GetMousePos())) + { + if (!m_Maximized) + { + m_OldPosition = ImGui::GetWindowPos(); + m_OldSize = ImGui::GetWindowSize(); + + auto monitor = ImGui::GetPlatformIO().Monitors[window->Viewport->PlatformMonitor]; + ImGui::SetWindowPos(windowTitle, monitor.WorkPos); + ImGui::SetWindowSize(windowTitle, monitor.WorkSize); + } + else + { + ImGui::SetWindowPos(windowTitle, m_OldPosition); + ImGui::SetWindowSize(windowTitle, m_OldSize); + } + + m_Maximized = !m_Maximized; + } + } + + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Options")) + { + ImGui::MenuItem("Show Cooking Results", nullptr, &m_ShowCookingResults); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Window")) + { + if (ImGui::MenuItem("Reset Layout")) + m_SelectedTabData->ResetDockspace = true; + + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // Rebuild Dockspace + if (m_RebuildDockspace) + { + m_RebuildDockspace = false; + m_DockSpaceID = ImGui::GetID("MeshColliderEditorDockspace"); + ImGui::DockBuilderRemoveNode(m_DockSpaceID); + ImGuiID rootNode = ImGui::DockBuilderAddNode(m_DockSpaceID, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderFinish(m_DockSpaceID); + } + + { + UI::ScopedColourStack dockspaceColors( + ImGuiCol_TitleBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f), + ImGuiCol_TitleBgActive, ImVec4(0.0f, 0.0f, 0.0f, 0.0f), + ImGuiCol_TitleBgCollapsed, ImVec4(0.0f, 0.0f, 0.0f, 0.0f), + ImGuiCol_TabActive, ImVec4(0.0f, 0.0f, 0.0f, 0.0f), // NOTE(Peter): Disable tab bar underline + ImGuiCol_TabUnfocusedActive, ImVec4(0.0f, 0.0f, 0.0f, 0.0f) // NOTE(Peter): Disable tab bar underline + ); + + ImGui::DockSpace(m_DockSpaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_NoCloseButton | ImGuiDockNodeFlags_NoWindowMenuButton); + } + + for (auto& [handle, tabData] : m_OpenTabs) + RenderTab(tabData); + + if ((uint64_t)m_ClosedTab != 0) + { + m_OpenTabs.erase(m_ClosedTab); + m_ClosedTab = AssetHandle(0); + } + + ImGui::End(); + Utils::PopMinSizeY(); + } + + if (!m_TabToFocus.empty()) + { + focusFrameCounter++; + + // NOTE(Peter): Workaround for ImGui windows not being focusable until after the second frame... + if (focusFrameCounter >= 2) + { + ImGui::SetWindowFocus(m_TabToFocus.c_str()); + m_TabToFocus.clear(); + focusFrameCounter = 0; + } + } + + if (m_OpenTabs.empty() || !m_WindowOpen) + { + if (!m_OpenTabs.empty()) + { + for (auto& [handle, tabData] : m_OpenTabs) + { + if (tabData->NeedsSaving) + AssetImporter::Serialize(tabData->ColliderAsset); + + DestroyTab(tabData); + } + } + + m_WindowOpen = false; + m_OpenTabs.clear(); + m_SelectedTabData = nullptr; + } + } + + void MeshColliderEditor::RenderTab(const std::shared_ptr& tabData) + { + SE_PROFILE_FUNCTION("MeshColliderEditor::RenderTab"); + const std::string dockspaceName = std::format("##{}-DockSpace", tabData->Name); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 2.0f)); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse; + + if (tabData->NeedsSaving) + flags |= ImGuiWindowFlags_UnsavedDocument; + + bool drawTab = ImGui::Begin(tabData->Name.c_str(), &tabData->IsOpen, flags); + ImGui::PopStyleVar(2); + + if (drawTab) + { + m_SelectedTabData = tabData; + + if (tabData->ResetDockspace) + { + tabData->DockSpaceID = ImGui::GetID(dockspaceName.c_str()); + ImGui::DockBuilderDockWindow(tabData->Name.c_str(), m_DockSpaceID); + + ImGui::DockBuilderRemoveNode(tabData->DockSpaceID); + + ImGuiID rootNode = ImGui::DockBuilderAddNode(tabData->DockSpaceID, ImGuiDockNodeFlags_DockSpace); + ImGuiID dockLeft; + ImGuiID dockRight = ImGui::DockBuilderSplitNode(rootNode, ImGuiDir_Right, 0.5f, nullptr, &dockLeft); + + ImGui::DockBuilderDockWindow(tabData->ViewportPanelName.c_str(), dockLeft); + ImGui::DockBuilderDockWindow(tabData->SettingsPanelName.c_str(), dockRight); + ImGui::DockBuilderSetNodeSize(dockRight, ImVec2(ImGui::GetContentRegionAvail().x / 2.0f, ImGui::GetContentRegionAvail().y)); + ImGui::DockBuilderFinish(tabData->DockSpaceID); + + tabData->ResetDockspace = false; + } + + Utils::PushMinSizeX(300.0f); + ImGui::DockSpace(tabData->DockSpaceID); + + RenderViewportPanel(tabData); + RenderSettingsPanel(tabData); + + Utils::PopMinSizeX(); + } + + if (!tabData->IsOpen) + { + AssetImporter::Serialize(tabData->ColliderAsset); + m_ClosedTab = tabData->ColliderAsset->Handle; + + if (m_SelectedTabData == tabData) + m_SelectedTabData = nullptr; + + DestroyTab(tabData); + } + + ImGui::End(); + } + + void MeshColliderEditor::RenderViewportPanel(const std::shared_ptr& tabData) + { + SE_PROFILE_FUNCTION("MeshColliderEditor::RenderViewportPanel"); + UI::ScopedStyle windowPadding(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin(tabData->ViewportPanelName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse); + + tabData->ViewportDockID = ImGui::GetWindowDockID(); + tabData->ViewportHovered = ImGui::IsWindowHovered(); + tabData->ViewportFocused = ImGui::IsWindowFocused(); + + auto minBound = ImGui::GetWindowPos(); + auto windowSize = ImGui::GetWindowSize(); + ImVec2 maxBound = { minBound.x + windowSize.x, minBound.y + windowSize.y }; + + auto viewportOffset = ImGui::GetCursorPos(); // includes tab bar + minBound.x += viewportOffset.x; + minBound.y += viewportOffset.y; + + ImVec2 viewportSize = { maxBound.x - minBound.x, maxBound.y - minBound.y }; + + if (viewportSize.x > 0 && viewportSize.y > 0) + { + tabData->ViewportScene->SetViewportBounds((uint32_t)minBound.x, (uint32_t)minBound.y, (uint32_t)maxBound.x, (uint32_t)maxBound.y); + tabData->ViewportRenderer->SetViewportSize((uint32_t)viewportSize.x, (uint32_t)viewportSize.y); + tabData->ViewportCamera.SetViewportBounds((uint32_t)minBound.x, (uint32_t)minBound.y, (uint32_t)maxBound.x, (uint32_t)maxBound.y); + tabData->ViewportCamera.SetPerspectiveProjectionMatrix(glm::radians(45.0f), viewportSize.x, viewportSize.y, 0.1f, 1000.0f); + + Ref finalImage = tabData->ViewportRenderer->GetFinalPassImage(); + if (finalImage) + UI::Image(finalImage, viewportSize, { 0, 1 }, { 1, 0 }); + } + + // Shadow + { + ImGuiDockNode* viewportDockNode = ImGui::DockBuilderGetNode(tabData->ViewportDockID); + ImGuiDockNode* settingsDockNode = ImGui::DockBuilderGetNode(tabData->SettingsDockID); + + if (viewportDockNode != nullptr && settingsDockNode != nullptr) + { + ImVec2 viewportPos = viewportDockNode->Pos; + ImVec2 settingsPos = settingsDockNode->Pos; + + if (viewportPos.y == settingsPos.y) + { + bool isLeftOfSettings = viewportPos.x < settingsPos.x; + ImRect windowRect = ImGui::GetCurrentWindow()->Rect(); + ImGui::PushClipRect(windowRect.Min, windowRect.Max, false); + UI::DrawShadowInner(EditorResources::ShadowTexture, 20, windowRect, 1.0f, windowRect.GetHeight() / 4.0f, !isLeftOfSettings, isLeftOfSettings, false, false); + ImGui::PopClipRect(); + } + else + { + bool isAboveSettings = viewportPos.y < settingsPos.y; + ImRect windowRect = ImGui::GetCurrentWindow()->Rect(); + ImGui::PushClipRect(windowRect.Min, windowRect.Max, false); + UI::DrawShadowInner(EditorResources::ShadowTexture, 20, windowRect, 1.0f, windowRect.GetHeight() / 4.0f, false, false, !isAboveSettings, isAboveSettings); + ImGui::PopClipRect(); + } + } + } + + ImGui::End(); + } + + void MeshColliderEditor::RenderSettingsPanel(const std::shared_ptr& tabData) + { + SE_PROFILE_FUNCTION("MeshColliderEditor::RenderSettingsPanel"); + const auto& meshMetadata = Project::GetEditorAssetManager()->GetMetadata(tabData->ColliderAsset->ColliderMesh); + const bool hasValidMesh = meshMetadata.IsValid() && (meshMetadata.Type == AssetType::Mesh || meshMetadata.Type == AssetType::StaticMesh); + + ImGui::Begin(tabData->SettingsPanelName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse); + tabData->SettingsDockID = ImGui::GetWindowDockID(); + + // Mesh Selection + if(UI::PropertyGridHeader("Mesh")) + { + UI::BeginPropertyGrid(); + AssetHandle oldMeshHandle = tabData->ColliderAsset->ColliderMesh; + tabData->NeedsCooking |= UI::PropertyMultiAssetReference("Collider Mesh", tabData->ColliderAsset->ColliderMesh, "Specifies what mesh this collider will use."); + + // Workaround having to cook the mesh in order for the new mesh to appear + if (oldMeshHandle != tabData->ColliderAsset->ColliderMesh) + UpdatePreviewEntity(tabData); + + UI::EndPropertyGrid(); + ImGui::TreePop(); + + // NOTE(Peter): Changing material won't require us to cook the collider again + if (hasValidMesh && UI::PropertyGridHeader("Collider Material")) + { + UI::BeginPropertyGrid(); + tabData->NeedsSaving |= UI::Property("Friction", tabData->ColliderAsset->Material.Friction, 0.1f, 0.0f, 0.0f, "Specifies the friction of this collision mesh. (Default: 0.5)"); + + tabData->NeedsSaving |= UI::Property("Restitution", tabData->ColliderAsset->Material.Restitution, 0.1f, 0.0f, 0.0f, "Specifies the restitution (bounciness) of this collision mesh. (Default: 0.15)"); + + UI::EndTreeNode(); + UI::EndPropertyGrid(); + } + } + + // Collider Settings + if (hasValidMesh && UI::PropertyGridHeader("Collider Settings")) + { + UI::BeginPropertyGrid(); + tabData->NeedsSaving |= UI::Property("Always Share Shape", tabData->ColliderAsset->AlwaysShareShape, "Forces ALL entities that use this collider to share the collider data as opposed to making copies of it. (Default: False)"); + + static const char* s_ColliderUsageOptions[] = { "Default", "Use Complex as Simple", "Use Simple as Complex" }; + tabData->NeedsCooking |= UI::PropertyDropdown("Collision Complexity", s_ColliderUsageOptions, 3, tabData->ColliderAsset->CollisionComplexity, "Specifies the collision complexity of the collider mesh."); + tabData->NeedsCooking |= UI::Property("Scale", tabData->ColliderAsset->ColliderScale, 0.1f, 0.0f, 0.0f, + "The scale of the collider. This value is a scalar of the entity scale. (Default: [1, 1, 1])"); + UI::EndPropertyGrid(); + ImGui::TreePop(); + } + + // Cooking settings + if (hasValidMesh) + { + if(UI::PropertyGridHeader("Cooking Settings")) + { + UI::BeginPropertyGrid(); + + tabData->NeedsCooking |= UI::Property("Vertex Welding", tabData->ColliderAsset->EnableVertexWelding, "Enables Vertex Welding, which will merge multiple vertices into a single vertex if they are withing a given distance. (Default: True)"); + UI::BeginDisabled(!tabData->ColliderAsset->EnableVertexWelding); + tabData->NeedsCooking |= UI::Property("Weld Tolerance", tabData->ColliderAsset->VertexWeldTolerance, 0.05f, 0.05f, 1.0f, + "Weld Tolerance controls how close 2 vertices has to be to each other before they merge. (Default: 0.1)"); + tabData->ColliderAsset->VertexWeldTolerance = glm::max(0.05f, tabData->ColliderAsset->VertexWeldTolerance); + UI::EndDisabled(); + + UI::EndPropertyGrid(); + ImGui::TreePop(); + } + + if (tabData->ColliderAsset->CollisionComplexity != ECollisionComplexity::UseComplexAsSimple && UI::PropertyGridHeader("Simple Collider Settings")) + { + UI::BeginPropertyGrid(); + tabData->NeedsCooking |= UI::Property("Check Zero-Area Triangles", tabData->ColliderAsset->CheckZeroAreaTriangles, + "If enabled, any triangle with an area less than the \"Zero-Area Threshold\" will not be included. (Default: True)"); + + UI::BeginDisabled(!tabData->ColliderAsset->CheckZeroAreaTriangles); + tabData->NeedsCooking |= UI::Property("Zero-Area Threshold", tabData->ColliderAsset->AreaTestEpsilon, + 0.1f, 0.0f, 0.0f, "Defines the minimum area a triangle can have before getting discarded. (Default: 0.06)"); + UI::EndDisabled(); + + tabData->NeedsCooking |= UI::Property("Shift Vertices to Origin", tabData->ColliderAsset->ShiftVerticesToOrigin, + "If enabled the vertices will be shifted to be around the origin. Only enable if you encounter issues. (Default: False)"); + + UI::EndPropertyGrid(); + ImGui::TreePop(); + } + + if (tabData->ColliderAsset->CollisionComplexity != ECollisionComplexity::UseSimpleAsComplex && UI::PropertyGridHeader("Complex Collider Settings")) + { + UI::BeginPropertyGrid(); + tabData->NeedsCooking |= UI::Property("Flip Normals", tabData->ColliderAsset->FlipNormals, + "If enabled the vertices will have their normals flipped. Effectively changing the winding order of the triangles. (Default: False)"); + UI::EndPropertyGrid(); + ImGui::TreePop(); + } + + // Preview Settings + if(UI::PropertyGridHeader("Preview Settings")) + { + UI::BeginPropertyGrid(); + tabData->NeedsSaving |= UI::Property("Scale", tabData->ColliderAsset->PreviewScale, 0.1f, 0.0f, 0.0f, + "Modifies the scale of the preview entity. Does NOT affect the collider in any way. (Default: [1, 1, 1])"); + tabData->ColliderEntity.GetComponent().Scale = tabData->ColliderAsset->PreviewScale; + UI::EndPropertyGrid(); + ImGui::PopID(); + } + + UI::ScopedStyle framePadding(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 5.0f)); + + UI::BeginDisabled(!tabData->NeedsCooking); + if (ImGui::Button("Cook Mesh")) + { + CookMeshCollider(tabData); + UpdatePreviewEntity(tabData); + tabData->NeedsSaving = true; + } + UI::SetTooltip("\"Cooks\" this collider asset, meaning it will generate the collider data."); + UI::EndDisabled(); + + ImGui::SameLine(); + + UI::BeginDisabled(!tabData->NeedsSaving); + if (ImGui::Button("Save")) + Save(tabData); + UI::EndDisabled(); + } + + //NOTE(Tim): This section of the code is needed to correctly set the layout when the asset is first created, + // because the tabs have to rendered for 1 frame for ResetDockspace to work properly. + if (tabData->JustCreated) + { + tabData->ResetDockspace = true; + tabData->JustCreated = false; + } + + ImGui::End(); + + RenderCookingOutput(); + } + + void MeshColliderEditor::UpdatePreviewEntity(const std::shared_ptr& tabData) + { + SE_PROFILE_FUNCTION("MeshColliderEditor::UpdatePreviewEntity"); + const auto& colliderMetadata = Project::GetEditorAssetManager()->GetMetadata(tabData->ColliderAsset->ColliderMesh); + + if (!colliderMetadata.IsValid()) + return; + + if (colliderMetadata.Type == AssetType::StaticMesh) + { + tabData->ColliderEntity.RemoveComponentIfExists(); + tabData->ColliderEntity.RemoveComponentIfExists(); + tabData->ColliderEntity.AddComponent(tabData->ColliderAsset->ColliderMesh); + + tabData->ColliderEntity.RemoveComponentIfExists(); + tabData->ColliderEntity.AddComponent(tabData->ColliderAsset->Handle); + } + else if (colliderMetadata.Type == AssetType::Mesh) + { + /*tabData->ColliderEntity.RemoveComponentIfExists(); + tabData->ColliderEntity.RemoveComponentIfExists(); + tabData->ColliderEntity.AddComponent(tabData->ColliderAsset->ColliderMesh);*/ + + tabData->ViewportScene->DestroyEntity(tabData->ColliderEntity); + tabData->ColliderEntity = tabData->ViewportScene->InstantiateMesh(AssetManager::GetAsset(tabData->ColliderAsset->ColliderMesh)); + + auto view = tabData->ViewportScene->GetAllEntitiesWith(); + for (auto enttID : view) + { + Entity entity = { enttID, tabData->ViewportScene.Raw() }; + auto& meshCollider = entity.AddComponent(tabData->ColliderAsset->Handle); + meshCollider.SubmeshIndex = entity.GetComponent().SubmeshIndex; + } + } + } + + bool MeshColliderEditor::CookMeshCollider(const std::shared_ptr& tabData) + { + //std::tie(m_LastSimpleCookingResult, m_LastComplexCookingResult) = CookingFactory::CookMesh(tabData->ColliderAsset, true); + //tabData->NeedsCooking = false; + + //// If using the default behavior both colliders must successfully cook + //if (tabData->ColliderAsset->CollisionComplexity == ECollisionComplexity::Default && m_LastSimpleCookingResult == ECookingResult::Success && m_LastComplexCookingResult == ECookingResult::Success) + //{ + // return true; + //} + + //// If using UseSimpleAsComplex only the simple collider has to cook successfully + //if (tabData->ColliderAsset->CollisionComplexity == ECollisionComplexity::UseSimpleAsComplex && m_LastSimpleCookingResult == ECookingResult::Success) + //{ + // return true; + //} + + //// If using UseComplexAsSimple only the complex collider has to cook successfully + //if (tabData->ColliderAsset->CollisionComplexity == ECollisionComplexity::UseComplexAsSimple && m_LastComplexCookingResult == ECookingResult::Success) + //{ + // return true; + //} + + //if (!m_ShowCookingResults) + // return false; + + //if (m_LastCookedTabData) + // m_LastCookedTabData.reset(); + + //m_LastCookedTabData = std::make_shared(); + //m_LastCookedTabData->Name = tabData->Name; + //m_LastCookedTabData->ColliderAsset = tabData->ColliderAsset; + //m_IsCookingResultsOpen = true; + return false; + } + + void MeshColliderEditor::RenderCookingOutput() + { + SE_PROFILE_FUNCTION("MeshColliderEditor::RenderCookingOutput"); + if (m_IsCookingResultsOpen && !ImGui::IsPopupOpen("Mesh Collider Cooking Output")) + { + ImGui::OpenPopup("Mesh Collider Cooking Output"); + } + + ImGui::SetNextWindowSize(ImVec2(600, 0)); + if (ImGui::BeginPopupModal("Mesh Collider Cooking Output", &m_IsCookingResultsOpen)) + { + const auto& colliderMeshMetadata = Project::GetEditorAssetManager()->GetMetadata(m_LastCookedTabData->ColliderAsset->ColliderMesh); + const std::string filename = colliderMeshMetadata.FilePath.filename().string(); + + auto getMessageForFailedCookingResult = [](ECookingResult cookingResult) + { + switch (cookingResult) + { + case StarEngine::ECookingResult::ZeroAreaTestFailed: return "Failed to find 4 initial vertices without a small triangle."; + case StarEngine::ECookingResult::PolygonLimitReached: + return "Successfully cooked mesh collider, but mesh polygon count exceeds 255. Either reduce the number of polygons(or use a simplified mesh for the collider), or enable the \"Quantize Input\" option."; + case StarEngine::ECookingResult::LargeTriangle: return "Failed to cook triangle mesh because one or more triangles are too large. Tessellate the mesh to reduce triangle size."; + case StarEngine::ECookingResult::InvalidMesh: return "Failed to cook mesh because an invalid mesh was provided. Please make sure you provide a valid mesh."; + case StarEngine::ECookingResult::Failure: return "An unknown error occurred. Please check the output logs."; + } + + return ""; + }; + + // Simple Collider + if (UI::PropertyGridHeader("Simple Collider")) + { + UI::Fonts::PushFont("Bold"); + if (m_LastSimpleCookingResult == ECookingResult::Success) + ImGui::Text("Successfully cooked simple mesh collider for %s", filename.c_str()); + else + ImGui::Text("Failed to cook simple mesh collider for %s", filename.c_str()); + UI::Fonts::PopFont(); + + if (m_LastSimpleCookingResult == ECookingResult::Success) + { + const auto& colliderData = PhysicsSystem::GetMeshCache().GetMeshData(m_LastCookedTabData->ColliderAsset); + ImGui::Text("Submeshes: %d", colliderData.SimpleColliderData.Submeshes.size()); + } + else + { + const auto& physxMessage = PhysicsSystem::GetLastErrorMessage(); + ImGui::TextWrapped("Message: %s\nPhysX Message: %s", getMessageForFailedCookingResult(m_LastSimpleCookingResult), physxMessage.c_str()); + } + + UI::EndTreeNode(); + } + + // Complex Collider + if (UI::PropertyGridHeader("Complex Collider")) + { + UI::Fonts::PushFont("Large"); + if (m_LastSimpleCookingResult == ECookingResult::Success) + ImGui::Text("Successfully cooked complex mesh collider for %s", filename.c_str()); + else + ImGui::Text("Failed to cook complex mesh collider for %s", filename.c_str()); + UI::Fonts::PopFont(); + + if (m_LastComplexCookingResult == ECookingResult::Success) + { + const auto& colliderData = PhysicsSystem::GetMeshCache().GetMeshData(m_LastCookedTabData->ColliderAsset); + ImGui::Text("Submeshes: %d", colliderData.ComplexColliderData.Submeshes.size()); + } + else + { + const auto& physxMessage = PhysicsSystem::GetLastErrorMessage(); + ImGui::TextWrapped("Message: %s\nPhysX Message: %s", getMessageForFailedCookingResult(m_LastComplexCookingResult), physxMessage.c_str()); + } + + UI::EndTreeNode(); + } + + ImGui::EndPopup(); + } + } + + void MeshColliderEditor::DestroyTab(const std::shared_ptr& tabData) + { + tabData->ViewportScene->DestroyEntity(tabData->ColliderEntity); + tabData->ViewportScene->DestroyEntity(tabData->DirectionalLight); + AssetHandle environmentHandle = tabData->SkyLight.GetComponent().SceneEnvironment; + auto environmentAsset = AssetManager::GetAsset(environmentHandle); + Project::GetEditorAssetManager()->RemoveAsset(environmentHandle); + tabData->ViewportScene->DestroyEntity(tabData->SkyLight); + tabData->ViewportRenderer->SetScene(nullptr); + + tabData->ViewportRenderer = nullptr; + tabData->ViewportScene = nullptr; + tabData->ColliderAsset = nullptr; + } + + void MeshColliderEditor::Save(const std::shared_ptr& tabData) + { + if (tabData->NeedsCooking) + CookMeshCollider(tabData); + + auto view = m_EditorScene->GetAllEntitiesWith(); + for (auto entityID : view) + { + Entity entity = { entityID, m_EditorScene.Raw() }; + auto& meshCollider = entity.GetComponent(); + + if (meshCollider.ColliderAsset != tabData->ColliderAsset->Handle) + continue; + + meshCollider.UseSharedShape = tabData->ColliderAsset->AlwaysShareShape; + meshCollider.CollisionComplexity = tabData->ColliderAsset->CollisionComplexity; + } + + AssetImporter::Serialize(tabData->ColliderAsset); + + UpdatePreviewEntity(tabData); + tabData->NeedsSaving = false; + } + +} diff --git a/StarEngine/src/StarEngine/Editor/MeshColliderEditor.h b/StarEngine/src/StarEngine/Editor/MeshColliderEditor.h new file mode 100644 index 00000000..0b5735c2 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/MeshColliderEditor.h @@ -0,0 +1,103 @@ +#pragma once + +#include "AssetEditorPanel.h" +#include "StarEngine/Asset/MeshColliderAsset.h" +#include "StarEngine/Renderer/SceneRenderer.h" +#include "StarEngine/Physics/PhysicsTypes.h" + +namespace StarEngine { + + class MeshColliderEditor : public AssetEditor + { + private: + struct MeshColliderTabData + { + std::string Name; + Ref ColliderAsset = nullptr; + + Ref ViewportScene; + Ref ViewportRenderer; + Entity ColliderEntity; + Entity SkyLight; + Entity DirectionalLight; + EditorCamera ViewportCamera{ 45.0f, 1280.0f, 720.0f, 0.1f, 1000.0f }; + + bool ViewportFocused = false; + bool ViewportHovered = false; + + // Docking Stuff + bool ResetDockspace = true; + ImGuiID DockSpaceID = 0; + ImGuiID SettingsDockID = 0; + ImGuiID ViewportDockID = 0; + + std::string ViewportPanelName; + std::string SettingsPanelName; + + bool IsOpen = true; + bool JustCreated = true; + bool NeedsSaving = false; + bool NeedsCooking = false; + }; + + public: + MeshColliderEditor(); + ~MeshColliderEditor(); + + virtual void OnUpdate(Timestep ts) override; + virtual void OnEvent(Event& e) override; + virtual void SetSceneContext(const Ref& context) override + { + if (!context) + { + m_EditorScene = nullptr; + return; + } + + if (context->IsEditorScene()) + m_EditorScene = context; + } + + virtual void OnImGuiRender() override; + + virtual void SetAsset(const Ref& asset) override; + + virtual void OnOpen() override {} + virtual void Render() override {} + virtual void OnClose() override {} + + private: + void RenderTab(const std::shared_ptr& tabData); + void RenderViewportPanel(const std::shared_ptr& tabData); + void RenderSettingsPanel(const std::shared_ptr& tabData); + + void UpdatePreviewEntity(const std::shared_ptr& tabData); + bool CookMeshCollider(const std::shared_ptr& tabData); + void RenderCookingOutput(); + void DestroyTab(const std::shared_ptr& tabData); + + void Save(const std::shared_ptr& tabData); + + private: + std::unordered_map> m_OpenTabs; + AssetHandle m_ClosedTab = AssetHandle(0); + + std::shared_ptr m_SelectedTabData; + bool m_WindowOpen = false; + bool m_RebuildDockspace = true; + ImGuiID m_DockSpaceID = 0; + std::string m_TabToFocus = ""; + + bool m_ShowCookingResults = true; + bool m_IsCookingResultsOpen = false; + std::shared_ptr m_LastCookedTabData; + ECookingResult m_LastSimpleCookingResult = ECookingResult::None; + ECookingResult m_LastComplexCookingResult = ECookingResult::None; + + bool m_Maximized = false; + ImVec2 m_OldPosition, m_OldSize; + + Ref m_EditorScene; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/MeshViewerPanel.cpp b/StarEngine/src/StarEngine/Editor/MeshViewerPanel.cpp new file mode 100644 index 00000000..d16d28e2 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/MeshViewerPanel.cpp @@ -0,0 +1,385 @@ +#include "sepch.h" +#include "MeshViewerPanel.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Asset/MeshSerializer.h" +#include "StarEngine/ImGui/PropertyGrid.h" +#include "StarEngine/Math/Math.h" +#include "StarEngine/Renderer/SceneRenderer.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace StarEngine { + + namespace Utils { + glm::mat4 Mat4FromAIMatrix4x4(const aiMatrix4x4& matrix); + } + + MeshViewerPanel::MeshViewerPanel() + : AssetEditor("MeshViewerPanel") + { + + } + + MeshViewerPanel::~MeshViewerPanel() + { + } + + void MeshViewerPanel::OnUpdate(Timestep ts) + { + for (auto&& [name, sceneData] : m_OpenMeshes) + { + sceneData->m_Camera.SetActive(sceneData->m_ViewportPanelFocused); + sceneData->m_Camera.OnUpdate(ts); + // m_Scene->OnUpdate(ts); + if (sceneData->m_IsViewportVisible) + sceneData->m_Scene->OnRenderEditor(sceneData->m_SceneRenderer, sceneData->m_Camera); + } + } + + void MeshViewerPanel::OnEvent(Event& e) + { + for (auto&& [name, sceneData] : m_OpenMeshes) + { + if (sceneData->m_ViewportPanelMouseOver) + sceneData->m_Camera.OnEvent(e); + } + } + + void MeshViewerPanel::RenderViewport() + { + } + + namespace Utils { + + static std::stack s_MinXSizes, s_MinYSizes; + static std::stack s_MaxXSizes, s_MaxYSizes; + + static void PushMinSizeX(float minSize) + { + ImGuiStyle& style = ImGui::GetStyle(); + s_MinXSizes.push(style.WindowMinSize.x); + style.WindowMinSize.x = minSize; + } + + static void PopMinSizeX() + { + ImGuiStyle& style = ImGui::GetStyle(); + style.WindowMinSize.x = s_MinXSizes.top(); + s_MinXSizes.pop(); + } + + static void PushMinSizeY(float minSize) + { + ImGuiStyle& style = ImGui::GetStyle(); + s_MinYSizes.push(style.WindowMinSize.y); + style.WindowMinSize.y = minSize; + } + + static void PopMinSizeY() + { + ImGuiStyle& style = ImGui::GetStyle(); + style.WindowMinSize.y = s_MinYSizes.top(); + s_MinYSizes.pop(); + } + + } + + void MeshViewerPanel::OnImGuiRender() + { + if (m_OpenMeshes.empty() || !m_WindowOpen) + return; + + ImGuiIO& io = ImGui::GetIO(); + ImGuiStyle& style = ImGui::GetStyle(); + auto boldFont = io.Fonts->Fonts[0]; + auto largeFont = io.Fonts->Fonts[1]; + + const char* windowName = "Asset Viewer"; + // Dockspace + { + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin(windowName, &m_WindowOpen, window_flags); + ImGui::PopStyleVar(); + + auto window = ImGui::GetCurrentWindow(); + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + if (window->TitleBarRect().Contains(ImGui::GetMousePos())) + { + auto monitor = ImGui::GetPlatformIO().Monitors[window->Viewport->PlatformMonitor]; + ImGui::SetWindowPos(windowName, { monitor.WorkPos }); + ImGui::SetWindowSize(windowName, { monitor.WorkSize }); + } + } + + if (m_ResetDockspace) + { + m_ResetDockspace = false; + { + ImGuiID assetViewerDockspaceID = ImGui::GetID("AssetViewerDockspace"); + ImGui::DockBuilderRemoveNode(assetViewerDockspaceID); + ImGuiID rootNode = ImGui::DockBuilderAddNode(assetViewerDockspaceID, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderFinish(assetViewerDockspaceID); + } + } + + ImGuiID assetViewerDockspaceID = ImGui::GetID("AssetViewerDockspace"); + ImGui::DockSpace(assetViewerDockspaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + + for (auto&& [name, sceneData] : m_OpenMeshes) + { + RenderMeshTab(assetViewerDockspaceID, sceneData); + } + ImGui::End(); + } + + if (!m_TabToFocus.empty()) + { + ImGui::SetWindowFocus(m_TabToFocus.c_str()); + m_TabToFocus.clear(); + } + + // Window has been closed + if (!m_WindowOpen) + { + m_OpenMeshes.clear(); + } + + } + + void MeshViewerPanel::SetAsset(const Ref& asset) + { + Ref meshSource = asset.As(); + + auto path = Project::GetEditorAssetManager()->GetFileSystemPath(asset->Handle).string(); + size_t found = path.find_last_of("/\\"); + std::string name = found != std::string::npos ? path.substr(found + 1) : path; + found = name.find_last_of("."); + name = found != std::string::npos ? name.substr(0, found) : name; + + if (m_OpenMeshes.find(name) != m_OpenMeshes.end()) + { + ImGui::SetWindowFocus(name.c_str()); + m_WindowOpen = true; + return; + } + + auto& sceneData = m_OpenMeshes[name] = std::make_shared(); + sceneData->m_MeshSource = meshSource; + sceneData->m_Name = name; + sceneData->m_Scene = Ref::Create("MeshViewerPanel", true); + sceneData->m_MeshEntity = sceneData->m_Scene->CreateEntity("Mesh"); + sceneData->m_Mesh = Ref::Create(sceneData->m_MeshSource->Handle, /*generateColliders=*/false); + sceneData->m_MeshEntity.AddComponent(sceneData->m_Mesh->Handle); + sceneData->m_MeshEntity.AddComponent().DynamicSky = true; + + sceneData->m_DirectionaLight = sceneData->m_Scene->CreateEntity("DirectionalLight"); + sceneData->m_DirectionaLight.AddComponent(); + sceneData->m_DirectionaLight.GetComponent().SetRotationEuler(glm::radians(glm::vec3{80.0f, 10.0f, 0.0f})); + sceneData->m_SceneRenderer = Ref::Create(sceneData->m_Scene); + sceneData->m_SceneRenderer->SetShadowSettings(-15.0f, 15.0f, 0.95f); + + ResetCamera(sceneData->m_Camera); + m_TabToFocus = name.c_str(); + m_WindowOpen = true; + } + + void MeshViewerPanel::ResetCamera(EditorCamera& camera) + { + camera = EditorCamera(45.0f, 1280.0f, 720.0f, 0.1f, 1000.0f); + } + + void MeshViewerPanel::RenderMeshTab(ImGuiID dockspaceID, const std::shared_ptr& sceneData) + { + ImGui::PushID(sceneData->m_Name.c_str()); + const char* meshTabName = sceneData->m_Name.c_str(); + + std::string toolBarName = std::format("##{}-{}", meshTabName, "toolbar"); + std::string viewportPanelName = std::format("Viewport##{}", meshTabName); + std::string propertiesPanelName = std::format("Properties##{}", meshTabName); + + { + ImGui::Begin(meshTabName, nullptr, ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoCollapse); + + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Window")) + { + if (ImGui::MenuItem("Reset Layout")) + sceneData->m_ResetDockspace = true; + + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + + ImGuiID tabDockspaceID = ImGui::GetID("MeshViewerDockspace"); + if (sceneData->m_ResetDockspace) + { + sceneData->m_ResetDockspace = false; + + ImGui::DockBuilderDockWindow(meshTabName, dockspaceID); + + // Setup dockspace + ImGui::DockBuilderRemoveNode(tabDockspaceID); + + ImGuiID rootNode = ImGui::DockBuilderAddNode(tabDockspaceID, ImGuiDockNodeFlags_DockSpace); + ImGuiID dockDown; + ImGuiID dockUp = ImGui::DockBuilderSplitNode(rootNode, ImGuiDir_Up, 0.05f, nullptr, &dockDown); + ImGuiID dockLeft; + ImGuiID dockRight = ImGui::DockBuilderSplitNode(dockDown, ImGuiDir_Right, 0.5f, nullptr, &dockLeft); + + ImGui::DockBuilderDockWindow(toolBarName.c_str(), dockUp); + ImGui::DockBuilderDockWindow(viewportPanelName.c_str(), dockLeft); + ImGui::DockBuilderDockWindow(propertiesPanelName.c_str(), dockRight); + ImGui::DockBuilderFinish(tabDockspaceID); + } + + Utils::PushMinSizeX(370.0f); + ImGui::DockSpace(tabDockspaceID, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + + ImGuiWindowClass window_class; + window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_AutoHideTabBar; + ImGui::SetNextWindowClass(&window_class); + + ImGui::Begin(toolBarName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::Button("Tool A", ImVec2(64, 64)); + ImGui::SameLine(); + ImGui::Button("Tool B", ImVec2(64, 64)); + ImGui::SameLine(); + ImGui::Button("Tool C", ImVec2(64, 64)); + ImGui::End(); + + ImGui::SetNextWindowClass(&window_class); + ImGui::Begin(viewportPanelName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse); + + sceneData->m_ViewportPanelMouseOver = ImGui::IsWindowHovered(); + sceneData->m_ViewportPanelFocused = ImGui::IsWindowFocused(); + + auto minBound = ImGui::GetWindowPos(); + auto windowSize = ImGui::GetWindowSize(); + ImVec2 maxBound = { minBound.x + windowSize.x, minBound.y + windowSize.y }; + + auto viewportOffset = ImGui::GetCursorPos(); // includes tab bar + minBound.x += viewportOffset.x; + minBound.y += viewportOffset.y; + + ImVec2 viewportSize = { maxBound.x - minBound.x, maxBound.y - minBound.y }; + + if (viewportSize.y > 0) + { + sceneData->m_SceneRenderer->SetViewportSize((uint32_t)viewportSize.x, (uint32_t)viewportSize.y); + sceneData->m_Camera.SetPerspectiveProjectionMatrix(glm::radians(45.0f), viewportSize.x, viewportSize.y, 0.1f, 1000.0f); + sceneData->m_Camera.SetViewportBounds((uint32_t)minBound.x, (uint32_t)minBound.y, (uint32_t)maxBound.x, (uint32_t)maxBound.y); + + if (sceneData->m_SceneRenderer->GetFinalPassImage()) + { + UI::ImageButton(sceneData->m_SceneRenderer->GetFinalPassImage(), viewportSize, { 0, 1 }, { 1, 0 }); + sceneData->m_IsViewportVisible = ImGui::IsItemVisible(); + } + + m_ViewportBounds[0] = { minBound.x, minBound.y }; + m_ViewportBounds[1] = { maxBound.x, maxBound.y }; + } + ImGui::End(); + ImGui::PopStyleVar(); + } + + { + ImGui::Begin(propertiesPanelName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse); + DrawMeshNode(sceneData->m_MeshSource, sceneData->m_Mesh); + ImGui::End(); + } + + ImGui::End(); + Utils::PopMinSizeX(); + } + ImGui::PopID(); + } + + void MeshViewerPanel::DrawMeshNode(const Ref& meshSource, const Ref& mesh) + { +#if 0 + // Mesh Hierarchy + auto rootNode = meshSource->m_Scene->mRootNode; + MeshNodeHierarchy(meshSource, mesh, 0); + + if (ImGui::Button("Create Mesh")) + { + std::filesystem::path meshSourcePath = meshSource->GetFilePath(); + std::filesystem::path directory = meshSourcePath.parent_path(); + std::string filename = std::format("{0}.hmesh", meshSourcePath.stem().string()); + Ref serializedMesh = Project::GetEditorAssetManager()->CreateOrReplaceAsset(directory / filename, mesh); + } +#endif + } + + void MeshViewerPanel::MeshNodeHierarchy(const Ref& meshAsset, Ref mesh, uint32_t nodeIndex, const glm::mat4& parentTransform, uint32_t level) + { +#if 0 + glm::mat4 localTransform = Utils::Mat4FromAIMatrix4x4(node->mTransformation); + glm::mat4 transform = parentTransform * localTransform; + + SE_CORE_ASSERT(meshAsset->m_NodeMap.find(node) != meshAsset->m_NodeMap.end()); + auto& meshIndices = meshAsset->m_NodeMap.at(node); + if (!meshIndices.empty()) + { + ImGui::PushID(node->mName.C_Str()); + uint32_t meshIndex = meshIndices.front(); + auto& submeshes = mesh->GetSubmeshes(); + + bool checked = std::find(submeshes.begin(), submeshes.end(), meshIndex) != submeshes.end(); + if (ImGui::Checkbox("##checkbox", &checked)) + { + if (checked) + submeshes.emplace_back(meshIndex); + else + submeshes.erase(std::find(submeshes.begin(), submeshes.end(), meshIndex)); + } + ImGui::PopID(); + ImGui::SameLine(); + } + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (node->mNumChildren == 0) + flags |= ImGuiTreeNodeFlags_Leaf; + if (ImGui::TreeNodeEx(node->mName.C_Str(), flags)) + { +#if TRANSFORM_INFO + { + glm::vec3 translation, rotation, scale; + Math::DecomposeTransform(transform, translation, rotation, scale); + ImGui::Text("World Transform"); + ImGui::Text(" Translation: %.2f, %.2f, %.2f", translation.x, translation.y, translation.z); + ImGui::Text(" Scale: %.2f, %.2f, %.2f", scale.x, scale.y, scale.z); + } + { + glm::vec3 translation, rotation, scale; + Math::DecomposeTransform(transform, translation, rotation, scale); + ImGui::Text("Local Transform"); + ImGui::Text(" Translation: %.2f, %.2f, %.2f", translation.x, translation.y, translation.z); + ImGui::Text(" Scale: %.2f, %.2f, %.2f", scale.x, scale.y, scale.z); + } +#endif + + for (uint32_t i = 0; i < node->mNumChildren; i++) + MeshNodeHierarchy(meshAsset, mesh, node->mChildren[i], transform, level + 1); + + ImGui::TreePop(); + } +#endif + } + +} diff --git a/StarEngine/src/StarEngine/Editor/MeshViewerPanel.h b/StarEngine/src/StarEngine/Editor/MeshViewerPanel.h new file mode 100644 index 00000000..7c97bd9b --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/MeshViewerPanel.h @@ -0,0 +1,63 @@ +#pragma once + +#include "StarEngine/Renderer/Mesh.h" + +#include "StarEngine/Scene/Scene.h" + +#include "StarEngine/Editor/EditorCamera.h" + +#include "AssetEditorPanel.h" + +namespace StarEngine { + + // NOTE(Yan): this is SEVERELY WIP + class MeshViewerPanel : public AssetEditor + { + private: + struct MeshScene + { + Ref m_Scene; + Ref m_SceneRenderer; + EditorCamera m_Camera { 45.0f, 1280.0f, 720.0f, 0.1f, 1000.0f }; + std::string m_Name; + Ref m_MeshSource; + Ref m_Mesh; + Entity m_MeshEntity; + Entity m_DirectionaLight; + bool m_ViewportPanelFocused = false; + bool m_ViewportPanelMouseOver = false; + bool m_ResetDockspace = true; + bool m_IsViewportVisible = false; + }; + public: + MeshViewerPanel(); + ~MeshViewerPanel(); + + virtual void OnUpdate(Timestep ts) override; + virtual void OnEvent(Event& e) override; + void RenderViewport(); + virtual void OnImGuiRender() override; + + virtual void SetAsset(const Ref& asset) override; + + virtual void OnOpen() override {} + virtual void OnClose() override {} + virtual void Render() override {} + + void ResetCamera(EditorCamera& camera); + private: + void RenderMeshTab(ImGuiID dockspaceID, const std::shared_ptr& sceneData); + void DrawMeshNode(const Ref& meshAsset, const Ref& mesh); + void MeshNodeHierarchy(const Ref& meshAsset, Ref mesh, uint32_t nodeIndex, const glm::mat4& parentTransform = glm::mat4(1.0f), uint32_t level = 0); + private: + bool m_ResetDockspace = true; + + std::unordered_map> m_OpenMeshes; + glm::vec2 m_ViewportBounds[2]; + + bool m_WindowOpen = false; + + std::string m_TabToFocus; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeEditorModel.cpp b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeEditorModel.cpp new file mode 100644 index 00000000..a95d9265 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeEditorModel.cpp @@ -0,0 +1,1045 @@ +#include +#include "NodeEditorModel.h" +#include "NodeGraphEditor.h" +#include "NodeGraphUtils.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Utilities/ContainerUtils.h" + +#include "choc/text/choc_StringUtilities.h" +#include "imgui-node-editor/imgui_node_editor.h" + +#include + +namespace StarEngine { + //============================================================================= + /// IDs + +#pragma region IDs + + uintptr_t NodeEditorModel::GetIDFromString(const std::string& idString) const + { + std::stringstream stream(idString); + uintptr_t ret; + stream >> ret; + return ret; + } + + void NodeEditorModel::AssignPinIds(Node* node) + { + for (auto& input : node->Inputs) + { + input->ID = UUID(); + input->Kind = PinKind::Input; + input->NodeID = node->ID; + } + + + for (auto& output : node->Outputs) + { + output->ID = UUID(); + output->Kind = PinKind::Output; + output->NodeID = node->ID; + } + } + + void NodeEditorModel::OnNodeSpawned(Node* node) + { + if (node) + { + AssignPinIds(node); + GetNodes().push_back(node); + } + } + +#pragma endregion IDs + + + //============================================================================= + /// Find items + +#pragma region Find objects + + Node* NodeEditorModel::FindNode(UUID id) + { + for (auto node : GetNodes()) + if (node->ID == id) + return node; + + return nullptr; + } + + const Node* NodeEditorModel::FindNode(UUID id) const + { + for (auto node : GetNodes()) + if (node->ID == id) + return node; + + return nullptr; + } + + Link* NodeEditorModel::FindLink(UUID id) + { + for (auto& link : GetLinks()) + if (link.ID == id) + return &link; + + return nullptr; + } + + const Link* NodeEditorModel::FindLink(UUID id) const + { + for (auto& link : GetLinks()) + if (link.ID == id) + return &link; + + return nullptr; + } + + Pin* NodeEditorModel::FindPin(UUID id) + { + if (!id) + return nullptr; + + for (auto& node : GetNodes()) + { + for (auto& pin : node->Inputs) + if (pin->ID == id) + return pin; + + for (auto& pin : node->Outputs) + if (pin->ID == id) + return pin; + } + + return nullptr; + } + + const Pin* NodeEditorModel::FindPin(UUID id) const + { + if (!id) + return nullptr; + + for (auto& node : GetNodes()) + { + for (auto& pin : node->Inputs) + if (pin->ID == id) + return pin; + + for (auto& pin : node->Outputs) + if (pin->ID == id) + return pin; + } + + return nullptr; + } + +#pragma endregion Find items + + + //============================================================================= + /// Links + +#pragma region Links + + Link* NodeEditorModel::GetLinkConnectedToPin(UUID id) + { + if (!id) + return nullptr; + + for (auto& link : GetLinks()) + if (link.StartPinID == id || link.EndPinID == id) + return &link; + + return nullptr; + } + + const Link* NodeEditorModel::GetLinkConnectedToPin(UUID id) const + { + if (!id) + return nullptr; + + for (auto& link : GetLinks()) + if (link.StartPinID == id || link.EndPinID == id) + return &link; + + return nullptr; + } + + const Node* NodeEditorModel::GetNodeForPin(UUID pinID) const + { + if (!pinID) + return nullptr; + + for (const auto& node : GetNodes()) + { + for (auto& pin : node->Inputs) + if (pin->ID == pinID) + return node; + + for (auto& pin : node->Outputs) + if (pin->ID == pinID) + return node; + } + + return nullptr; + } + + const Node* NodeEditorModel::GetNodeConnectedToPin(UUID pinID) const + { + const Pin* pin = FindPin(pinID); + if (!pin) return nullptr; + const Link* link = GetLinkConnectedToPin(pin->ID); + if (!link) return nullptr; + const Pin* otherPin = FindPin(pin->Kind == PinKind::Input ? link->StartPinID : link->EndPinID); + if (!otherPin) return nullptr; + + return FindNode(otherPin->NodeID); + } + + Node* NodeEditorModel::GetNodeConnectedToPin(UUID pinID) + { + Pin* pin = FindPin(pinID); + if (!pin) return nullptr; + Link* link = GetLinkConnectedToPin(pin->ID); + if (!link) return nullptr; + Pin* otherPin = FindPin(pin->Kind == PinKind::Input ? link->StartPinID : link->EndPinID); + if (!otherPin) return nullptr; + + return FindNode(otherPin->NodeID); + } + + bool NodeEditorModel::IsPinLinked(UUID pinID) + { + return GetLinkConnectedToPin(pinID) != nullptr; + } + + bool NodeEditorModel::IsPinLinked(UUID pinID) const + { + return GetLinkConnectedToPin(pinID) != nullptr; + } + + std::vector NodeEditorModel::GetAllLinksConnectedToPin(UUID pinID) + { + if (!pinID) + return {}; + + std::vector links; + + for (auto& link : GetLinks()) + if (link.StartPinID == pinID || link.EndPinID == pinID) + links.push_back(&link); + + return links; + } + + std::vector NodeEditorModel::GetAllLinkIDsConnectedToPin(UUID pinID) + { + if (!pinID) + return {}; + + std::vector links; + + for (auto& link : GetLinks()) + if (link.StartPinID == pinID || link.EndPinID == pinID) + links.push_back(link.ID); + + return links; + } + + void NodeEditorModel::DeleteDeadLinks(UUID nodeId) + { + auto wasConnectedToTheNode = [&](const Link& link) + { + return (!FindPin(link.StartPinID)) || (!FindPin(link.EndPinID)) + || FindPin(link.StartPinID)->NodeID == nodeId + || FindPin(link.EndPinID)->NodeID == nodeId; + }; + + auto& links = GetLinks(); + + // Clear pin value + std::for_each(links.begin(), links.end(), [&](Link& link) + { + auto pin = FindPin(link.StartPinID); + SE_CORE_ASSERT(pin); // this can fail if node implementation has been changed since the graph was first created + if (pin && pin->NodeID == nodeId) + { + if (auto* pin = FindPin(link.EndPinID)) + AssignSomeDefaultValue(pin); + } + }); + + auto removeIt = std::remove_if(links.begin(), links.end(), wasConnectedToTheNode); + const bool linkRemoved = removeIt != links.end(); + + links.erase(removeIt, links.end()); + + if (linkRemoved) + OnLinkDeleted(); + } + + void NodeEditorModel::AssignSomeDefaultValue(Pin* pin) + { + if (pin->Kind == PinKind::Output) // implementation specific + return; + + choc::value::Value v; + switch (pin->GetType()) + { + case EPinType::Bool: v = choc::value::Value(true); break; + case EPinType::Float: v = choc::value::Value(1.0f); break; + case EPinType::Int: v = choc::value::Value(int(0)); break; + case EPinType::Enum: v = choc::value::Value(int(0)); break; + case EPinType::String: v = choc::value::Value(""); break; + default: break; + } + + if (pin->Storage == StorageKind::Value) + pin->Value = v; + else + pin->Value = choc::value::createArray(1, [&v](uint32_t) { return v; }); + } + + NodeEditorModel::LinkQueryResult NodeEditorModel::CanCreateLink(Pin* startPin, Pin* endPin) + { + if (!startPin) return LinkQueryResult::InvalidStartPin; + else if (!endPin) return LinkQueryResult::InvalidEndPin; + else if (endPin == startPin) return LinkQueryResult::SamePin; + else if (endPin->NodeID == startPin->NodeID) return LinkQueryResult::SameNode; + else if (endPin->Kind == startPin->Kind) return LinkQueryResult::IncompatiblePinKind; + else if (endPin->Storage != startPin->Storage) return LinkQueryResult::IncompatibleStorageKind; + else if (!endPin->IsSameType(startPin)) return LinkQueryResult::IncompatibleType; + + return LinkQueryResult::CanConnect; + } + + void NodeEditorModel::CreateLink(Pin* startPin, Pin* endPin) + { + SE_CORE_ASSERT(startPin && endPin); + + auto& links = GetLinks(); + + links.emplace_back(startPin->ID, endPin->ID); + + // TODO: get static Types factory from CRTP type and get icon colour from it? + links.back().Color = GetNodeFactory()->GetIconColor(startPin->GetType()); + + // Clear pin value + endPin->Value = choc::value::Value(); //? this is not ideal, as it clears the type as well + + OnLinkCreated(links.back().ID); + } + + void NodeEditorModel::RemoveLink(UUID linkId) + { + auto& links = GetLinks(); + + auto id = std::find_if(links.begin(), links.end(), [linkId](auto& link) { return link.ID == linkId; }); + if (id != links.end()) + { + // Clear pin value + if (auto* endPin = FindPin(id->EndPinID)) + AssignSomeDefaultValue(endPin); + + links.erase(id); + + OnLinkDeleted(); + } + } + + void NodeEditorModel::RemoveLinks(const std::vector& linkIds) + { + for (const auto& linkId : linkIds) + RemoveLink(linkId); + } + +#pragma endregion Links + + + //============================================================================= + /// Nodes + +#pragma region Nodes + + void NodeEditorModel::RemoveNode(UUID nodeId) + { + auto& nodes = GetNodes(); + + auto it = std::find_if(nodes.begin(), nodes.end(), [nodeId](auto& node) { return node->ID == nodeId; }); + + if (it != nodes.end()) + { + DeleteDeadLinks(nodeId); + delete *it; + nodes.erase(it); + OnNodeDeleted(); + } + } + + void NodeEditorModel::RemoveNodes(const std::vector& nodeIds) + { + auto& nodes = GetNodes(); + + for (const auto& id : nodeIds) + DeleteDeadLinks(id); + + auto it = std::stable_partition(nodes.begin(), nodes.end(), [&nodeIds](auto& node) + { + for (const auto& id : nodeIds) + { + if (node->ID == id) + return false; + } + return true; + }); + + if (it != nodes.end()) + { + std::for_each(it, nodes.end(), [](auto node) { delete node; }); + nodes.erase(it, nodes.end()); + OnNodeDeleted(); + } + } + + Node* NodeEditorModel::CreateNode(const std::string& category, const std::string& typeID) + { + if (auto* newNode = GetNodeFactory()->SpawnNode(category, typeID)) + { + OnNodeSpawned(newNode); + OnNodeCreated(); + return GetNodes().back(); + } + else + { + return nullptr; + } + } + +#pragma endregion Nodes + + + //============================================================================= + /// Serialization + +#pragma region Serialization + + void NodeEditorModel::SaveAll() + { + Serialize(); + } + + void NodeEditorModel::LoadAll() + { + Deserialize(); + } + +#pragma endregion Serialization + + //============================================================================= + /// Model Interface + + void NodeEditorModel::CompileGraph() + { + OnCompileGraph(); + } + + + //============================================================================= + // IO Node Editor Model + //============================================================================= + + std::string IONodeEditorModel::GetPropertyToken(EPropertyType type) + { + return Utils::SplitAtUpperCase(magic_enum::enum_name(type)); + } + + + EPropertyType IONodeEditorModel::GetPropertyType(std::string_view propertyToken) + { + if (propertyToken == GetPropertyToken(EPropertyType::Input)) + return EPropertyType::Input; + else if (propertyToken == GetPropertyToken(EPropertyType::Output)) + return EPropertyType::Output; + else if (propertyToken == GetPropertyToken(EPropertyType::LocalVariable)) + return EPropertyType::LocalVariable; + else + return EPropertyType::Invalid; + } + + + bool IONodeEditorModel::IsPropertyNode(EPropertyType type, const Node* node, std::string_view propertyName) + { + if (type == EPropertyType::LocalVariable ? node->Name != propertyName : node->Name != GetPropertyToken(type)) + { + return false; + } + + switch (type) + { + case EPropertyType::Input: // Graph Input node can only have one output + return node->Outputs.size() == 1 && node->Outputs[0]->Name == propertyName; + case EPropertyType::Output: // Graph Output node can only have one input + return node->Inputs.size() == 1 && node->Inputs[0]->Name == propertyName; + case EPropertyType::LocalVariable: // Graph Local Variable node can only have one input or output + return node->Outputs.empty() ? node->Inputs[0]->Name == propertyName : node->Outputs[0]->Name == propertyName; + } + + return false; + } + + + bool IONodeEditorModel::IsPropertyNode(const Node* node) const + { + return (node->Category == GetPropertyToken(EPropertyType::Input) + && node->Inputs.empty() + && GetPropertySet(EPropertyType::Input).HasValue(node->Outputs[0]->Name)) + || (node->Category == GetPropertyToken(EPropertyType::Output) + && node->Outputs.empty() + && GetPropertySet(EPropertyType::Output).HasValue(node->Inputs[0]->Name)) + || (node->Category == GetPropertyToken(EPropertyType::LocalVariable) + && GetPropertySet(EPropertyType::LocalVariable).HasValue(node->Outputs.empty() ? node->Inputs[0]->Name : node->Outputs[0]->Name)); + } + + + EPropertyType IONodeEditorModel::GetPropertyTypeOfNode(const Node* node) + { + if (!IsPropertyNode(node)) + return EPropertyType::Invalid; + + return GetPropertyType(node->Category); + } + + + bool IONodeEditorModel::IsLocalVariableNode(const Node* node) + { + return node->Category == GetPropertyToken(EPropertyType::LocalVariable) + && ((node->Inputs.size() == 1 && node->Outputs.size() == 0) + || (node->Inputs.size() == 0 && node->Outputs.size() == 1)); + } + + + bool IONodeEditorModel::IsSameLocalVariableNodes(const Node* nodeA, const Node* nodeB) + { + if (!IsLocalVariableNode(nodeA) || !IsLocalVariableNode(nodeB)) + return false; + + if (&nodeA == &nodeB) + return true; + + return (nodeA->Inputs.empty() ? nodeA->Outputs[0]->Name : nodeA->Inputs[0]->Name) + == (nodeB->Inputs.empty() ? nodeB->Outputs[0]->Name : nodeB->Inputs[0]->Name); + } + + + const Pin* IONodeEditorModel::FindLocalVariableSource(std::string_view localVariableName) const + { + const Node* setterNode = nullptr; + + // Find setter node for the local variable + for (auto& node : GetNodes()) + { + if (IsLocalVariableNode(node) && node->Name == localVariableName && node->Inputs.size() == 1) + { + setterNode = node; + break; + } + } + + if (!setterNode) + return nullptr; + + // find link and pin connected to the input pin of the setter node + if (auto* link = GetLinkConnectedToPin(setterNode->Inputs[0]->ID)) + return FindPin(link->StartPinID); + + return nullptr; + } + + + Utils::PropertySet& IONodeEditorModel::GetPropertySet(EPropertyType type) + { + switch (type) + { + case EPropertyType::Input: return GetInputs(); + case EPropertyType::Output: return GetOutputs(); + case EPropertyType::LocalVariable: return GetLocalVariables(); + case EPropertyType::Invalid: + default: SE_CORE_ASSERT(false); return GetInputs(); + } + } + + + const Utils::PropertySet& IONodeEditorModel::GetPropertySet(EPropertyType type) const + { + switch (type) + { + case EPropertyType::Input: return GetInputs(); + case EPropertyType::Output: return GetOutputs(); + case EPropertyType::LocalVariable: return GetLocalVariables(); + case EPropertyType::Invalid: + default: SE_CORE_ASSERT(false); return GetInputs(); + } + } + + + std::string IONodeEditorModel::AddPropertyToGraph(EPropertyType type, const choc::value::ValueView& value) + { + std::string name; + + if (type == EPropertyType::Invalid) + { + SE_CORE_ASSERT(false); + return name; + } + + const auto getUniqueName = [&](const Utils::PropertySet& propertySet, std::string_view defaultName) + { + return Utils::AddSuffixToMakeUnique(std::string(defaultName), [&](const std::string& newName) + { + auto properties = propertySet.GetNames(); + return std::any_of(properties.begin(), properties.end(), [&newName](const std::string name) { return name == newName; }); + }); + }; + + auto& properties = GetPropertySet(type); + + switch (type) + { + case EPropertyType::Input: + name = getUniqueName(properties, "New Input"); + properties.Set(name, value); + break; + case EPropertyType::Output: + name = getUniqueName(properties, "New Output"); + properties.Set(name, value); + break; + case EPropertyType::LocalVariable: + name = getUniqueName(properties, "New Variable"); + properties.Set(name, value); + break; + default: + break; + } + properties.Sort(); + + InvalidatePlayer(); + + //? DBG + //SE_CORE_WARN("Added Input {} to the graph. Inputs:", name); + //SE_LOG_TRACE(m_GraphInputs.ToJSON()); + + return name; + } + + + void IONodeEditorModel::RemovePropertyFromGraph(EPropertyType type, const std::string& name) + { + if (type == EPropertyType::Invalid) + { + SE_CORE_ASSERT(false); + return; + } + + auto& properties = GetPropertySet(type); + properties.Remove(name); + + std::vector nodesToDelete; + + for (const auto& node : GetNodes()) + { + if (IsPropertyNode(type, node, name)) + nodesToDelete.push_back(node->ID); + } + + RemoveNodes(nodesToDelete); + + InvalidatePlayer(); + + //? DBG + //SE_CORE_WARN("Removed Input {} from the graph. Inputs:", name); + } + + + void IONodeEditorModel::SetPropertyValue(EPropertyType type, const std::string& propertyName, const choc::value::ValueView& value) + { + if (type == EPropertyType::Invalid) + { + SE_CORE_ASSERT(false); + return; + } + + auto& properties = GetPropertySet(type); + if (properties.HasValue(propertyName)) + { + properties.Set(propertyName, value); + OnGraphPropertyValueChanged(type, propertyName); + } + } + + + void IONodeEditorModel::RenameProperty(EPropertyType type, const std::string& oldName, const std::string& newName) + { + if (type == EPropertyType::Invalid) + { + SE_CORE_ASSERT(false); + return; + } + + auto& properties = GetPropertySet(type); + if (properties.HasValue(oldName)) + { + choc::value::Value value = properties.GetValue(oldName); + + properties.Remove(oldName); + properties.Set(newName, value); + properties.Sort(); + + OnGraphPropertyNameChanged(type, oldName, newName); + } + } + + + void IONodeEditorModel::ChangePropertyType(EPropertyType type, const std::string& propertyName, const choc::value::ValueView& valueOfNewType) + { + if (type == EPropertyType::Invalid) + { + SE_CORE_ASSERT(false); + return; + } + + auto& properties = GetPropertySet(type); + if (properties.HasValue(propertyName)) + { + choc::value::Value value = properties.GetValue(propertyName); + if (value.getType() != valueOfNewType.getType()) + { + properties.Set(propertyName, valueOfNewType); + + OnGraphPropertyTypeChanged(type, propertyName); + } + } + } + + + void IONodeEditorModel::OnGraphPropertyNameChanged(EPropertyType type, const std::string& oldName, const std::string& newName) + { + if (type == EPropertyType::Invalid) + return; + + const auto getPropertyValuePin = [](Node* node, std::string_view propertyName) -> Pin* + { + SE_CORE_ASSERT(!node->Outputs.empty() || !node->Inputs.empty()); + + if (!node->Outputs.empty()) + return node->Outputs[0]; + else if (!node->Inputs.empty()) + return node->Inputs[0]; + + return nullptr; + }; + + bool renamed = false; + + for (auto& node : GetNodes()) + { + if (IsPropertyNode(type, node, oldName)) + { + if (Pin* pin = getPropertyValuePin(node, oldName)) + { + pin->Name = newName; + + if (pin->Kind == PinKind::Output) + { + if (Link* link = GetLinkConnectedToPin(pin->ID)) + { + if (Pin* endPin = FindPin(link->EndPinID)) + endPin->Value = choc::value::createString(newName); + } + } + + renamed = true; + } + + if (type == EPropertyType::LocalVariable) + { + node->Name = newName; + renamed = true; + } + } + } + + if (renamed) + InvalidatePlayer(); + } + + + void IONodeEditorModel::OnGraphPropertyTypeChanged(EPropertyType type, const std::string& inputName) + { + if (type == EPropertyType::Invalid) + return; + + auto newProperty = GetPropertySet(type).GetValue(inputName); + + const bool isArray = newProperty.isArray(); + auto [pinType, storageKind] = GetNodeFactory()->GetPinTypeAndStorageKindForValue(newProperty); + + const auto getPropertyValuePin = [](Node* node, std::string_view propertyName) -> Pin** + { + SE_CORE_ASSERT(!node->Outputs.empty() || !node->Inputs.empty()); + + if (!node->Outputs.empty()) + return &node->Outputs[0]; + else if (!node->Inputs.empty()) + return &node->Inputs[0]; + + return nullptr; + }; + + bool pinChanged = false; + + for (auto& node : GetNodes()) + { + if (IsPropertyNode(type, node, inputName)) + { + if (Pin** pin = getPropertyValuePin(node, inputName)) + { + // create new pin of the new type + Pin* newPin = GetNodeFactory()->CreatePinForType(pinType); + + // copy data from the old pin to the new pin + *newPin = **pin; + + // delete old pin + delete (*pin); + + // emplace new pin instead of the old pin + *pin = newPin; + + //pin->Type = pinType; + newPin->Storage = storageKind; + newPin->Value = newProperty; // TODO: should use one or the other + node->Color = GetValueColor(newProperty); + + if (Link* connectedLink = GetLinkConnectedToPin(newPin->ID)) + { + ax::NodeEditor::DeleteLink(ax::NodeEditor::LinkId(connectedLink->ID)); + RemoveLink(connectedLink->ID); + // TODO: add visual indication for the user that the node pin has been disconnected, or that the link is invalid + } + + pinChanged = true; + } + } + } + + if (pinChanged) + InvalidatePlayer(); + } + + + void IONodeEditorModel::OnGraphPropertyValueChanged(EPropertyType type, const std::string& inputName) + { + if (type == EPropertyType::Invalid) + return; + + auto newValue = GetPropertySet(type).GetValue(inputName); + const auto& valueType = newValue.getType(); + + // If wave asset input changed, need to initialize audio data readers for new files + // which we do when recompiling. + // TODO: perhaps we could somehow do a clean swap of the readers while the player is playing? + if (Utils::IsAssetHandle(valueType) || (valueType.isArray() && Utils::IsAssetHandle(valueType.getElementType()))) + { + InvalidatePlayer(); + } + + // TODO (0x): when we have animation graph preview, we will also need to InvalidatePlayer() for other asset types (such as animation clip) + + const auto getPropertyValuePin = [](Node* node, std::string_view propertyName) -> Pin* + { + SE_CORE_ASSERT(!node->Outputs.empty() || !node->Inputs.empty()); + + if (!node->Outputs.empty()) + return node->Outputs[0]; + else if (!node->Inputs.empty()) + return node->Inputs[0]; + + return nullptr; + }; + + // Set Outputting value for the "Graph Input" node + for (auto& node : GetNodes()) + { + if (IsPropertyNode(type, node, inputName)) + { + if (Pin* pin = getPropertyValuePin(node, inputName)) + pin->Value = newValue; + } + } + + if (type == EPropertyType::LocalVariable) + InvalidatePlayer(); + + } + + + // TODO: move this out to some sort of SoundGraph Utils header + namespace Keyword { + // TODO: change these for the new SoundGraph +#define KEYWORDS(X) \ + X("if") X("do") X("for") X("let") \ + X("var") X("int") X("try") X("else") \ + X("bool") X("true") X("case") X("enum") \ + X("loop") X("void") X("while") X("break") \ + X("const") X("int32") X("int64") X("float") \ + X("false") X("using") X("fixed") X("graph") \ + X("input") X("event") X("class") X("catch") \ + X("throw") X("output") X("return") X("string") \ + X("struct") X("import") X("switch") X("public") \ + X("double") X("private") X("float32") X("float64") \ + X("default") X("complex") X("continue") X("external") \ + X("operator") X("processor") X("namespace") X("complex32") \ + X("complex64") X("connection") + + struct Matcher + { + static bool Match(std::string_view name) noexcept + { +#define COMPARE_KEYWORD(str) if (name.length() == (int) sizeof (str) - 1 && choc::text::startsWith(name, (str))) return true; + KEYWORDS(COMPARE_KEYWORD) +#undef COMPARE_KEYWORD + return false; + } + }; + } + + + bool IONodeEditorModel::IsGraphPropertyNameValid(EPropertyType type, std::string_view name) const noexcept + { + const auto isPropertyNameValid = [&](const Utils::PropertySet& propertySet) + { + return !propertySet.HasValue(name) + && !propertySet.HasValue(choc::text::replace(std::string(name), " ", "")) + && !Keyword::Matcher::Match(choc::text::replace(std::string(name), " ", "")); + }; + + switch (type) + { + case EPropertyType::Invalid: + return false; + case EPropertyType::Input: + return isPropertyNameValid(GetInputs()); + case EPropertyType::Output: + return isPropertyNameValid(GetOutputs()); + case EPropertyType::LocalVariable: + return isPropertyNameValid(GetLocalVariables()); + default: + return false; + } + } + + Node* IONodeEditorModel::FindNodeByName(std::string_view name) + { + for (auto& node : GetNodes()) + { + if (node->Name == name) + return node; + } + return nullptr; + } + + + NodeEditorModel::LinkQueryResult IONodeEditorModel::CanCreateLink(Pin * startPin, Pin * endPin) + { + LinkQueryResult result = NodeEditorModel::CanCreateLink(startPin, endPin); + + if (!result) + return result; + + auto startNode = FindNode(startPin->NodeID); + auto endNode = FindNode(endPin->NodeID); + + if (!startNode) return LinkQueryResult::InvalidStartPin; + if (!endNode) return LinkQueryResult::InvalidEndPin; + + // TODO: in the future might need to change this to check if the nodes are from the same LV + if (IsLocalVariableNode(startNode) && IsLocalVariableNode(endNode)) + { + if (startPin->Name == endPin->Name) + return LinkQueryResult::CausesLoop; + } + + return LinkQueryResult::CanConnect; + } + + + void IONodeEditorModel::OnNodeCreated() + { + InvalidatePlayer(); + + if (onNodeCreated) + onNodeCreated(); + } + + void IONodeEditorModel::OnNodeDeleted() + { + InvalidatePlayer(); + + if (onNodeDeleted) + onNodeDeleted(); + } + + void IONodeEditorModel::OnLinkCreated(UUID linkID) + { + if (auto* newLink = FindLink(linkID)) + { + auto* startPin = FindPin(newLink->StartPinID); + auto* endPin = FindPin(newLink->EndPinID); + SE_CORE_ASSERT(endPin); + + auto* startNode = FindNode(startPin->NodeID); + SE_CORE_ASSERT(startNode); + + auto* endNode = FindNode(endPin->NodeID); + SE_CORE_ASSERT(startPin && endNode); + + //? JP. no idea why we needed this :shrug: +#if 0 + // If pin is connected to Graph Input getter node, + // set its name as the value for this pin + const bool isGraphInput = startNode->Category == GetPropertyToken(EPropertyType::Input); + if (isGraphInput && GetInputs().HasValue(startPin->Name)) + { + endPin->Value = choc::value::Value(startPin->Name); + if (onPinValueChanged) + onPinValueChanged(endNode->ID, endPin->ID); + } +#endif + + // TODO: handle connections to Local Variables? + //else if (m_GraphAsset->LocalVariables.HasValue(startPin->Name)) + + // Remove any other links connected to the same input pin + // For Sound Graph nodes we only allow single input connection, + // but multiple output connections. + std::vector linkIds = GetAllLinkIDsConnectedToPin(newLink->EndPinID); + Utils::Remove(linkIds, newLink->ID); + RemoveLinks(linkIds); + } + + InvalidatePlayer(); + + if (onLinkCreated) + onLinkCreated(linkID); + } + + void IONodeEditorModel::OnLinkDeleted() + { + InvalidatePlayer(); + + if (onLinkDeleted) + onLinkDeleted(); + } + +} // namespace Hazel diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeEditorModel.h b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeEditorModel.h new file mode 100644 index 00000000..67ec0dec --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeEditorModel.h @@ -0,0 +1,226 @@ +#pragma once + +#include "Nodes.h" +#include "PropertySet.h" + +#include "StarEngine/Asset/AssetImporter.h" + +#include + +#include + +namespace StarEngine { + + class NodeEditorModel + { + public: + NodeEditorModel(const NodeEditorModel&) = delete; + NodeEditorModel& operator=(const NodeEditorModel&) = delete; + + NodeEditorModel() = default; + virtual ~NodeEditorModel() = default; + + //================================================================================== + /// Graph Model Interface + virtual std::vector& GetNodes() = 0; + virtual std::vector& GetLinks() = 0; + virtual const std::vector& GetNodes() const = 0; + virtual const std::vector& GetLinks() const = 0; + + virtual const Nodes::Registry& GetNodeTypes() const = 0; + + //================================================================================== + /// Graph Model + uintptr_t GetIDFromString(const std::string& idString) const; + + ImColor GetIconColor(int pinTypeID) const { return GetNodeFactory()->GetIconColor(pinTypeID); } + ImColor GetValueColor(const choc::value::Value& v) const { return GetNodeFactory()->GetValueColor(v); } + + Node* FindNode(UUID id); + Link* FindLink(UUID id); + Pin* FindPin(UUID id); + const Node* FindNode(UUID id) const; + const Link* FindLink(UUID id) const; + const Pin* FindPin(UUID id) const; + + Link* GetLinkConnectedToPin(UUID pinID); + const Link* GetLinkConnectedToPin(UUID pinID) const; + const Node* GetNodeForPin(UUID pinID) const; + const Node* GetNodeConnectedToPin(UUID pinID) const; // Get node on the other side of the link + Node* GetNodeConnectedToPin(UUID pinID); // Get node on the other side of the link + bool IsPinLinked(UUID pinID); + bool IsPinLinked(UUID pinID) const; + + std::vector GetAllLinksConnectedToPin(UUID pinID); + std::vector GetAllLinkIDsConnectedToPin(UUID pinID); + + struct LinkQueryResult + { + enum + { + CanConnect, + InvalidStartPin, + InvalidEndPin, + IncompatiblePinKind, + IncompatibleStorageKind, + IncompatibleType, + SamePin, + SameNode, + CausesLoop + } Reason; + + LinkQueryResult(decltype(Reason) value) : Reason(value) {} + + explicit operator bool() + { + return Reason == LinkQueryResult::CanConnect; + } + }; + virtual LinkQueryResult CanCreateLink(Pin* startPin, Pin* endPin); + void CreateLink(Pin* startPin, Pin* endPin); + void RemoveLink(UUID linkId); + void RemoveLinks(const std::vector& linkIds); + + Node* CreateNode(const std::string& category, const std::string& typeID); + void RemoveNode(UUID nodeId); + void RemoveNodes(const std::vector& nodeIds); + + void SaveAll(); + void LoadAll(); + virtual bool SaveGraphState(const char* data, size_t size) = 0; + virtual const std::string& LoadGraphState() = 0; + + void CompileGraph(); + + private: + //================================================================================== + /// Internal + void AssignPinIds(Node* node); + void DeleteDeadLinks(UUID nodeId); + + // Used when deleting links to reassign some sort of valid default value + virtual void AssignSomeDefaultValue(Pin* pin); + + protected: + // Adds node to the list of nodes + void OnNodeSpawned(Node* node); + + // Sort nodes in order that makes sense + virtual bool Sort() { return false; } + + //================================================================================== + /// Graph Model Interface + public: + using NodeID = UUID; + using PinID = UUID; + using LinkID = UUID; + + std::function onNodeCreated = nullptr; + std::function onNodeDeleted = nullptr; + std::function onLinkCreated = nullptr; + std::function onLinkDeleted = nullptr; + + std::function onPinValueChanged = nullptr; + + std::function onEditNode = nullptr; + + std::function graphAsset)> onCompile = nullptr; + std::function onCompiledSuccessfully = nullptr; + std::function onPlay = nullptr; + std::function onStop = nullptr; + + protected: + virtual void OnNodeCreated() { if (onNodeCreated) onNodeCreated(); } + virtual void OnNodeDeleted() { if (onNodeDeleted) onNodeDeleted(); } + virtual void OnLinkCreated(UUID linkID) { if (onLinkCreated) onLinkCreated(linkID); } + virtual void OnLinkDeleted() { if (onLinkDeleted) onLinkDeleted(); } + + virtual void OnCompileGraph() {}; + + virtual const Nodes::AbstractFactory* GetNodeFactory() const = 0; + + private: + virtual void Serialize() = 0; + virtual void Deserialize() = 0; + }; + + + inline bool operator==(NodeEditorModel::LinkQueryResult a, NodeEditorModel::LinkQueryResult b) + { + return a.Reason == b.Reason; + } + + + inline bool operator!=(NodeEditorModel::LinkQueryResult a, NodeEditorModel::LinkQueryResult b) + { + return !(a == b); + } + + + // Extend node editor model with concept of "Inputs" and "Outputs" + // Used for both Animation and Sound graphs + class IONodeEditorModel : public NodeEditorModel + { + public: + // names of types of graph input. e.g. { "Float", "Int", "Bool", "String", "WaveAsset" } + virtual const std::vector& GetInputTypes() const = 0; + + virtual Utils::PropertySet& GetInputs() = 0; + virtual Utils::PropertySet& GetOutputs() = 0; + virtual Utils::PropertySet& GetLocalVariables() = 0; + virtual const Utils::PropertySet& GetInputs() const = 0; + virtual const Utils::PropertySet& GetOutputs() const = 0; + virtual const Utils::PropertySet& GetLocalVariables() const = 0; + + static std::string GetPropertyToken(EPropertyType type); + static EPropertyType GetPropertyType(std::string_view propertyToken); + static bool IsPropertyNode(EPropertyType type, const Node* node, std::string_view propertyName); + static bool IsLocalVariableNode(const Node* node); + static bool IsSameLocalVariableNodes(const Node* nodeA, const Node* nodeB); + const Pin* FindLocalVariableSource(std::string_view localVariableName) const; + + EPropertyType GetPropertyTypeOfNode(const Node* node); + /** @returns true if any of the property sets contain property with the name of the node or its out/in pin. */ + bool IsPropertyNode(const Node* node) const; + + virtual Utils::PropertySet& GetPropertySet(EPropertyType type); + virtual const Utils::PropertySet& GetPropertySet(EPropertyType type) const; + + virtual std::string AddPropertyToGraph(EPropertyType type, const choc::value::ValueView& value); + virtual void RemovePropertyFromGraph(EPropertyType type, const std::string& name); + virtual void SetPropertyValue(EPropertyType type, const std::string& propertyName, const choc::value::ValueView& value); + virtual void RenameProperty(EPropertyType type, const std::string& oldName, const std::string& newName); + virtual void ChangePropertyType(EPropertyType type, const std::string& propertyName, const choc::value::ValueView& valueOfNewType); + + virtual void OnGraphPropertyNameChanged(EPropertyType type, const std::string& oldName, const std::string& newName); + virtual void OnGraphPropertyTypeChanged(EPropertyType type, const std::string& inputName); + virtual void OnGraphPropertyValueChanged(EPropertyType type, const std::string& inputName); + + // TODO: SpawnGraphOutputNode + virtual Node* SpawnGraphInputNode(const std::string& inputName) = 0; + virtual Node* SpawnLocalVariableNode(const std::string& inputName, bool getter) = 0; + + bool IsPlayerDirty() const noexcept { return m_PlayerDirty; } + + // Flag player dirty. Dirty player needs to be recompiled + // and parameters changes can't be applied live. + virtual void InvalidatePlayer(const bool isDirty = true) { m_PlayerDirty = isDirty; } + + virtual bool IsGraphPropertyNameValid(EPropertyType type, std::string_view name) const noexcept; + + protected: + Node* FindNodeByName(std::string_view name); + + LinkQueryResult CanCreateLink(Pin* startPin, Pin* endPin) override; + + void OnNodeCreated() override; + void OnNodeDeleted() override; + void OnLinkCreated(UUID linkID) override; + void OnLinkDeleted() override; + + private: + bool m_PlayerDirty = true; + }; + +} // namespace StarEngine + diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditor.cpp b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditor.cpp new file mode 100644 index 00000000..560c7f1a --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditor.cpp @@ -0,0 +1,2682 @@ +#include "sepch.h" +#include "NodeGraphEditor.h" + +#include "NodeEditorModel.h" +#include "NodeGraphEditorContext.h" +#include "NodeGraphUtils.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Core/Input.h" +#include "StarEngine/Editor/EditorResources.h" +#include "StarEngine/ImGui/Colors.h" +#include "StarEngine/ImGui/ImGuiUtilities.h" +#include "StarEngine/ImGui/ImGuiWidgets.h" +#include "StarEngine/vendor/imgui-node-editor/widgets.h" + +#include +#include +#include + +namespace ed = ax::NodeEditor; +namespace utils = ax::NodeEditor::Utilities; +using NodeBuilder = utils::BlueprintNodeBuilder; + +static ax::NodeEditor::Detail::EditorContext* GetEditorContext() +{ + return (ax::NodeEditor::Detail::EditorContext*)(ed::GetCurrentEditor()); +} + +namespace StarEngine +{ + + struct EnumPinContext + { + UUID PinID = 0; + int SelectedValue = 0; + + /* Implementation might want to store enum value as a member of an object, or as int32. + This callback allows implementation to construct choc::Value from selected item on change. + */ + std::function ConstructValue; + }; + + static EnumPinContext s_SelectedEnumContext; + + + //================================================================= + /// NodeEditor Draw Utilities +#pragma region Node Editor Draw Utilities + + float NodeEditorDraw::GetBestWidthForEnumCombo(const std::vector& enumTokens) + { + float width = 40.0f; + for (const auto& token : enumTokens) + { + const float strWidth = ImGui::CalcTextSize(token.name.data()).x; + if (strWidth > width) + width = strWidth; + } + return width + GImGui->Style.WindowPadding.x * 2.0f; + } + + + bool NodeEditorDraw::PropertyBool(choc::value::Value& value) + { + bool modified = false; + + bool pinValue = !value.isVoid() ? value.get() : false; + + if (UI::Checkbox("##bool", &pinValue)) + { + value = choc::value::createBool(pinValue); + modified = true; + } + + return modified; + } + + + bool NodeEditorDraw::PropertyFloat(choc::value::Value& value, const char* format) + { + bool modified = false; + + float pinValue = !value.isVoid() ? value.get() : 0.0f; + + char v_str[64]; + ImFormatString(v_str, IM_ARRAYSIZE(v_str), format, pinValue); + const float valueWidth = ImGui::CalcTextSize(v_str).x + GImGui->Style.FramePadding.y * 2.0f; + ImGui::PushItemWidth(glm::max(valueWidth, 40.0f)); + + if (UI::DragFloat("##floatIn", &pinValue, 0.01f, 0.0f, 0.0f, format)) + { + value = choc::value::createFloat32(pinValue); + modified = true; + } + + ImGui::PopItemWidth(); + + return modified; + } + + + bool NodeEditorDraw::PropertyInt(choc::value::Value& value, const char* format) + { + bool modified = false; + + int pinValue = !value.isVoid() ? value.get() : 0; + + char v_str[64]; + ImFormatString(v_str, IM_ARRAYSIZE(v_str), format, pinValue); + const float valueWidth = ImGui::CalcTextSize(v_str).x + GImGui->Style.FramePadding.y * 2.0f; + ImGui::PushItemWidth(glm::max(valueWidth, 30.0f)); + + if (UI::DragInt32("##int", &pinValue, 1.0f, 0, 0, format)) + { + value = choc::value::createInt32(pinValue); + modified = true; + } + + ImGui::PopItemWidth(); + + return modified; + } + + + bool NodeEditorDraw::PropertyVec3(std::string_view label, choc::value::Value& value, const char* format) + { + bool modified = false; + bool manuallyEdited = false; + + glm::vec3 pinValue = CustomValueTo(value).value_or(glm::vec3{}); + + char v_str[64]; + ImFormatString(v_str, IM_ARRAYSIZE(v_str), format, 9999.99f); + float valueWidth = ImGui::CalcTextSize(v_str).x; + ImFormatString(v_str, IM_ARRAYSIZE(v_str), format, pinValue.x); + valueWidth = glm::max(valueWidth, ImGui::CalcTextSize(v_str).x); + ImFormatString(v_str, IM_ARRAYSIZE(v_str), format, pinValue.y); + valueWidth = glm::max(valueWidth, ImGui::CalcTextSize(v_str).x); + ImFormatString(v_str, IM_ARRAYSIZE(v_str), format, pinValue.z); + valueWidth = glm::max(valueWidth, ImGui::CalcTextSize(v_str).x); + valueWidth = (valueWidth + 4.0f + GImGui->Font->FontSize + 6.0f) * 3.0f; + + if (UI::Widgets::EditVec3(label, ImVec2(valueWidth, ImGui::GetFrameHeightWithSpacing() + 8.0f), 0.0f, manuallyEdited, pinValue, UI::VectorAxis::None, 1.0f, glm::zero(), glm::zero(), format)) + { + value = ValueFrom(pinValue); + modified = true; + } + + return modified; + } + + bool NodeEditorDraw::PropertyString(choc::value::Value& value) + { + bool modified = false; + + std::string valueStr; + + if (!value.isVoid()) + { + valueStr = value.get(); + } + + ImGui::PushItemWidth(100.0f); + + const auto inputTextFlags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue; + UI::InputText("##edit", &valueStr, inputTextFlags); + + ImGui::PopItemWidth(); + + if (ImGui::IsItemDeactivatedAfterEdit()) + { + value = choc::value::createString(valueStr); + modified = true; + } + + return modified; + } + + + bool NodeEditorDraw::PropertyEnum(int enumValue, Pin* pin, bool& openEnumPopup, std::function constructValue) + { + SE_CORE_VERIFY(pin); + + bool modified = false; + + int pinValue = enumValue; + + std::optional*> optTokens = pin->EnumTokens; + if (optTokens.has_value() && optTokens.value() != nullptr) + { + SE_CORE_ASSERT(pinValue >= 0); + UI::ScopedColour bg(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_FrameBg)); + + const auto& tokens = *optTokens.value(); + + ImGui::SetNextItemWidth(GetBestWidthForEnumCombo(tokens)); + if (UI::FakeComboBoxButton("##fakeCombo", tokens[pinValue].name.data())) + { + openEnumPopup = true; + s_SelectedEnumContext = { pin->ID, pinValue }; + s_SelectedEnumContext.ConstructValue = constructValue; + } + } + + return modified; + } +#pragma endregion + + + NodeGraphEditorBase::NodeGraphEditorBase(const char* id) + : AssetEditor(id) + { + m_State = new ContextState; + m_CanvasName = "Canvas##" + m_Id; + m_NodeBuilderTexture = EditorResources::TranslucentGradientTexture; + } + + NodeGraphEditorBase::~NodeGraphEditorBase() { + delete m_State; + } + + + void NodeGraphEditorBase::SetAsset(const Ref& asset) + { + const AssetMetadata& metadata = Project::GetEditorAssetManager()->GetMetadata(asset->Handle); + SetTitle(metadata.FilePath.stem().string()); + } + + + std::vector NodeGraphEditorBase::GetSelectedNodeIDs() const + { + std::vector selectedNodes; + selectedNodes.resize(ed::GetSelectedObjectCount()); + int nodeCount = ed::GetSelectedNodes(selectedNodes.data(), static_cast(selectedNodes.size())); + selectedNodes.resize(nodeCount); + + std::vector ids; + for (const auto& nodeID : selectedNodes) + ids.push_back(nodeID.Get()); + + return ids; + } + + + bool NodeGraphEditorBase::InitializeEditor() + { + ed::Config config; + + config.CanvasSizeMode = ed::CanvasSizeMode::CenterOnly; + config.SettingsFile = nullptr; + config.UserPointer = this; + + // Limiting to 1:1 doesn't allow sufficient zoom on a high DPI monitor. + // Can revisit this if/when Hazelnut properly supports high DPI. + // + //// Set up custom zeoom levels + //{ + // // Limitting max zoom in to 1:1 + // static constexpr float zooms[] = { 0.1f, 0.15f, 0.20f, 0.25f, 0.33f, 0.5f, 0.75f, 1.0f }; + // for (uint32_t i = 0; i < sizeof(zooms) / sizeof(*zooms); ++i) + // config.CustomZoomLevels.push_back(zooms[i]); + //} + + // Save graph state + config.SaveSettings = [](const char* data, size_t size, ed::SaveReasonFlags reason, void* userPointer) -> bool + { + auto* nodeEditor = static_cast(userPointer); + return nodeEditor->GetModel()->SaveGraphState(data, size); + }; + + // Load graph state + config.LoadSettings = [](char* data, void* userPointer) -> size_t + { + auto* nodeEditor = static_cast(userPointer); + const std::string& graphState = nodeEditor->GetModel()->LoadGraphState(); + if (data) + { + memcpy(data, graphState.data(), graphState.size()); + } + return graphState.size(); + }; + + // Restore node location + config.LoadNodeSettings = [](ed::NodeId nodeId, char* data, void* userPointer) -> size_t + { + auto* nodeEditor = static_cast(userPointer); + + if (!nodeEditor->GetModel()) + return 0; + + auto node = nodeEditor->GetModel()->FindNode(UUID(nodeId.Get())); + if (!node) + return 0; + + + if (data != nullptr) + memcpy(data, node->State.data(), node->State.size()); + return node->State.size(); + }; + + // Store node location + config.SaveNodeSettings = [](ed::NodeId nodeId, const char* data, size_t size, ed::SaveReasonFlags reason, void* userPointer) -> bool + { + auto* nodeEditor = static_cast(userPointer); + + if (!nodeEditor->GetModel()) + return false; + + auto node = nodeEditor->GetModel()->FindNode(UUID(nodeId.Get())); + if (!node) + return false; + + node->State.assign(data, size); + + ////self->TouchNode(nodeId); + + return true; + }; + + // NOTE: ed::CreateEditor takes a deep copy of the passed in config + m_Editor = ed::CreateEditor(&config); + ed::SetCurrentEditor(m_Editor); + + InitializeEditorStyle(ed::GetStyle()); + + return true; + } + + + void NodeGraphEditorBase::Render() + { + if (!GetModel()) + return; + + if (!m_Editor || !m_Initialized) + { + m_Initialized = InitializeEditor(); + m_FirstFrame = true; + } + + m_MainDockID = ImGui::DockSpace(ImGui::GetID(m_Id.c_str()), ImVec2(0, 0), ImGuiDockNodeFlags_AutoHideTabBar); + + UI::ScopedStyle stylePadding(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 2.0f)); + + m_WindowClass.ClassId = ImGui::GetID(m_Id.c_str()); + m_WindowClass.DockingAllowUnclassed = false; + + ImGui::SetNextWindowDockID(m_MainDockID, ImGuiCond_Always); + ImGui::SetNextWindowClass(GetWindowClass()); + + if (ImGui::Begin(m_CanvasName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse)) + { + // Don't render graph on first frame. + // This gives ImGui a chance to layout the windows before we start drawing the graph. + // This is important as drawing the graph into incorrectly sized window causes + // the graph to save its settings with incorrect visible region. + if (!m_FirstFrame) + { + auto [editorMin, editorMax] = DrawGraph("Node Editor"); + + // Draw on top of canvas for subclasses + OnRenderOnCanvas(editorMin, editorMax); + } + } + ImGui::End(); // Canvas + + if (m_State->EditNodeId) + { + Node* node = GetModel()->FindNode(m_State->EditNodeId); + if (node) + { + if (auto onEditNode = GetModel()->onEditNode) + { + onEditNode(node); + m_FirstFrame = true; + } + } + m_State->EditNodeId = 0; + } + + // Draw windows of subclasses + OnRender(); + m_FirstFrame = false; + } + + + bool NodeGraphEditorBase::DeactivateInputIfDragging(const bool wasActive) + { + if (ImGui::IsItemActive() && !wasActive) + { + m_State->DraggingInputField = true; + ed::EnableShortcuts(false); + return true; + } + else if (!ImGui::IsItemActive() && wasActive) + { + m_State->DraggingInputField = false; + ed::EnableShortcuts(true); + return false; + } + return wasActive; + } + + + void NodeGraphEditorBase::OnClose() + { + if (auto* model = GetModel(); model) + { + if (model->onStop) + { + model->onStop(true); + } + model->SaveAll(); + } + + if (m_Editor) + { + // Destroy editor causes settings to be saved. + // However, saving the settings at this point results in bung settings. + // Disable saving. + // (the settings are saved every time the canvas is panned or zoomed anyway, so + // this is not missing out anything) + // + // imgui_node_editor api does not make this easy... + ed::SetCurrentEditor(m_Editor); + const_cast(GetEditorContext()->GetConfig()).SaveSettings = nullptr; + + ed::DestroyEditor(m_Editor); + m_Editor = nullptr; + } + } + + + void NodeGraphEditorBase::LoadSettings() + { + GetEditorContext()->LoadSettings(); + } + + + void NodeGraphEditorBase::InitializeEditorStyle(ax::NodeEditor::Style& editorStyle) + { + // Style + editorStyle.NodePadding = { 0.0f, 4.0f, 0.0f, 0.0f }; // This mostly affects Comment nodes + editorStyle.NodeRounding = 3.0f; + editorStyle.NodeBorderWidth = 1.0f; + editorStyle.HoveredNodeBorderWidth = 2.0f; + editorStyle.SelectedNodeBorderWidth = 3.0f; + editorStyle.PinRounding = 2.0f; + editorStyle.PinBorderWidth = 0.0f; + editorStyle.LinkStrength = 80.0f; + editorStyle.ScrollDuration = 0.35f; + editorStyle.FlowMarkerDistance = 30.0f; + editorStyle.FlowDuration = 2.0f; + editorStyle.GroupRounding = 0.0f; + editorStyle.GroupBorderWidth = 0.0f; + editorStyle.GridSnap = 8.0f; + + editorStyle.HighlightConnectedLinks = 1.0f; + + // Extra (for now just using defaults) + editorStyle.SnapLinkToPinDir = 0.0f; + editorStyle.PivotAlignment = ImVec2(0.5f, 0.5f); // This one is changed in Draw + editorStyle.PivotSize = ImVec2(0.0f, 0.0f); // This one is changed in Draw + editorStyle.PivotScale = ImVec2(1, 1); // This one is not used + editorStyle.PinCorners = ImDrawFlags_RoundCornersAll; + editorStyle.PinRadius = 0.0f; // This one is changed in Draw + editorStyle.PinArrowSize = 0.0f; // This one is changed in Draw + editorStyle.PinArrowWidth = 0.0f; // This one is changed in Draw + + // Colours + editorStyle.Colors[ed::StyleColor_Bg] = ImColor(23, 24, 28, 200); + editorStyle.Colors[ed::StyleColor_Grid] = ImColor(21, 21, 21, 255);// ImColor(60, 60, 60, 40); + editorStyle.Colors[ed::StyleColor_NodeBg] = ImColor(31, 33, 38, 255); + editorStyle.Colors[ed::StyleColor_NodeBorder] = ImColor(51, 54, 62, 140); + editorStyle.Colors[ed::StyleColor_HovNodeBorder] = ImColor(60, 180, 255, 150); + editorStyle.Colors[ed::StyleColor_SelNodeBorder] = ImColor(255, 225, 135, 255); + editorStyle.Colors[ed::StyleColor_NodeSelRect] = ImColor(5, 130, 255, 64); + editorStyle.Colors[ed::StyleColor_NodeSelRectBorder] = ImColor(5, 130, 255, 128); + editorStyle.Colors[ed::StyleColor_HovLinkBorder] = ImColor(60, 180, 255, 255); + editorStyle.Colors[ed::StyleColor_SelLinkBorder] = ImColor(255, 225, 135, 255); + editorStyle.Colors[ed::StyleColor_LinkSelRect] = ImColor(5, 130, 255, 64); + editorStyle.Colors[ed::StyleColor_LinkSelRectBorder] = ImColor(5, 130, 255, 128); + editorStyle.Colors[ed::StyleColor_PinRect] = ImColor(60, 180, 255, 0); + editorStyle.Colors[ed::StyleColor_PinRectBorder] = ImColor(60, 180, 255, 0); + editorStyle.Colors[ed::StyleColor_Flow] = ImColor(255, 128, 64, 255); + editorStyle.Colors[ed::StyleColor_FlowMarker] = ImColor(255, 128, 64, 255); + editorStyle.Colors[ed::StyleColor_GroupBg] = ImColor(255, 255, 255, 30); + editorStyle.Colors[ed::StyleColor_GroupBorder] = ImColor(0, 0, 0, 0); + + editorStyle.Colors[ed::StyleColor_HighlightLinkBorder] = ImColor(255, 255, 255, 140); + } + + + void NodeGraphEditorBase::EnsureWindowIsDocked(ImGuiWindow* childWindow) + { + if (childWindow->DockNode && childWindow->DockNode->ParentNode) + m_DockIDs[childWindow->ID] = childWindow->DockNode->ParentNode->ID; + + //? For some reason IsAnyMouseDown is not reporting correctly anymore + //? So for now "auto docking" is disabled + if (!childWindow->DockIsActive && !ImGui::IsAnyMouseDown() && !childWindow->DockNode && !childWindow->DockNodeIsVisible) + { + if (!m_DockIDs[childWindow->ID]) + m_DockIDs[childWindow->ID] = m_MainDockID; + + ImGui::SetWindowDock(childWindow, m_DockIDs[childWindow->ID], ImGuiCond_Always); + } + } + + + ImColor NodeGraphEditorBase::GetIconColor(int pinTypeID) const + { + return GetModel()->GetIconColor(pinTypeID); + }; + + + bool NodeGraphEditorBase::DrawPinPropertyEdit(PinPropertyContext& context) + { + bool modified = false; + + Pin* pin = context.pin; + + switch ((EPinType)pin->GetType()) + { + case EPinType::Bool: modified = NodeEditorDraw::PropertyBool(pin->Value); break; + case EPinType::Int: modified = NodeEditorDraw::PropertyInt(pin->Value); break; + case EPinType::Float: modified = NodeEditorDraw::PropertyFloat(pin->Value); break; + case EPinType::String: modified = NodeEditorDraw::PropertyString(pin->Value); break; + case EPinType::Enum: modified = NodeEditorDraw::PropertyEnum(pin->Value.get(), pin, context.OpenEnumPopup, + [](int selected) {return choc::value::createInt32(selected);}); break; + default: + SE_CORE_ASSERT(false, "Unhandled pin type") + break; + } + + return modified; + } + + + std::pair NodeGraphEditorBase::DrawGraph(const char* id) + { + // This ensures that we don't start selection rectangle when dragging Canvas window titlebar + bool draggingCanvasTitlebar = false; + if (ImGui::IsItemActive()) + draggingCanvasTitlebar = true; + + m_State->NewNodePopupOpening = false; + PinPropertyContext pinContext{ nullptr, GetModel(), false, false }; + + ed::Begin(id); + { + auto cursorTopLeft = ImGui::GetCursorScreenPos(); + DrawNodes(pinContext); + DrawDeferredComboBoxes(pinContext); + DrawLinks(); + + if (ed::BeginDelete()) + { + ed::LinkId linkId = 0; + while (ed::QueryDeletedLink(&linkId)) + { + if (ed::AcceptDeletedItem()) + GetModel()->RemoveLink(linkId.Get()); + } + + ed::NodeId nodeId = 0; + while (ed::QueryDeletedNode(&nodeId)) + { + if (ed::AcceptDeletedItem()) + GetModel()->RemoveNode(nodeId.Get()); + } + } + ed::EndDelete(); + + ImGui::SetCursorScreenPos(cursorTopLeft); + + ed::Suspend(); + CheckContextMenus(); + ed::Resume(); + + ed::Suspend(); + UI::ScopedStyleStack( + ImGuiStyleVar_WindowPadding, ImVec2(8, 8), + ImGuiStyleVar_FrameBorderSize, 0.0f + ); + UI::ScopedColourStack popupColours( + ImGuiCol_HeaderHovered, IM_COL32(0, 0, 0, 80), + ImGuiCol_Separator, IM_COL32(90, 90, 90, 255), + ImGuiCol_Text, Colors::Theme::textBrighter + ); + + if (UI::BeginPopup("Node Context Menu")) + { + auto node = GetModel()->FindNode(m_State->ContextNodeId.Get()); + DrawNodeContextMenu(node); + + UI::EndPopup(); + } + + if (UI::BeginPopup("Pin Context Menu")) + { + auto pin = GetModel()->FindPin(m_State->ContextPinId.Get()); + DrawPinContextMenu(pin); + + UI::EndPopup(); + } + + if (UI::BeginPopup("Link Context Menu")) + { + auto link = GetModel()->FindLink(m_State->ContextLinkId.Get()); + DrawLinkContextMenu(link); + + UI::EndPopup(); + } + + bool isNewNodePoppedUp = false; + static ImVec2 newNodePostion{ 0.0f, 0.0f }; + { + UI::ScopedStyle scrollListStyle(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f)); + if (UI::BeginPopup("Create New Node")) + { + DrawBackgroundContextMenu(isNewNodePoppedUp, newNodePostion); + auto* popupWindow = ImGui::GetCurrentWindow(); + auto shadowRect = ImRect(popupWindow->Pos, popupWindow->Pos + popupWindow->Size); + UI::EndPopup(); + UI::DrawShadow(EditorResources::ShadowTexture, 14, shadowRect, 1.3f); + } + else + { + if (m_State->CreateNewNode && onNewNodePopupCancelled) + onNewNodePopupCancelled(); + + m_State->CreateNewNode = false; + } + } + ed::Resume(); + + if (isNewNodePoppedUp && m_State->NewNodeLinkPinId) + { + if (const auto* newNodeLinkPin = GetModel()->FindPin(m_State->NewNodeLinkPinId)) + { + const float lineThickness = 2.0f; + auto& creator = GetEditorContext()->GetItemCreator(); + if (creator.m_IsActive) + creator.SetStyle(GetIconColor(newNodeLinkPin->GetType()), lineThickness); + + // this is needed to fix the case when if you move mouse fast when release dragged link, + // the link end drops along the way behind the actual popup position + auto& draggedPivot = newNodeLinkPin->Kind == PinKind::Output ? creator.m_lastEndPivot : creator.m_lastStartPivot; + const ImVec2 offset = newNodePostion - draggedPivot.GetCenter(); + draggedPivot.Translate(offset); + + ed::DrawLastLink(); + } + } + } + + ed::End(); + + if (onDragDropOntoCanvas) + { + if (ImGui::BeginDragDropTarget()) + { + onDragDropOntoCanvas(); + ImGui::EndDragDropTarget(); + } + } + + auto editorMin = ImGui::GetItemRectMin(); + auto editorMax = ImGui::GetItemRectMax(); + + //// Draw current zoom level + //{ + // UI::ScopedFont largeFont(ImGui::GetIO().Fonts->Fonts[1]); + // const ImVec2 zoomLabelPos({ editorMin.x + 20.0f, editorMax.y - 40.0f }); + // const std::string zoomLabelText = "Zoom " + choc::text::floatToString(ed::GetCurrentZoom(), 1, true); + // + // ImGui::GetWindowDrawList()->AddText(zoomLabelPos, IM_COL32(255, 255, 255, 60), zoomLabelText.c_str()); + //} + + UI::DrawShadowInner(EditorResources::ShadowTexture, 50, editorMin, editorMax, 0.3f, (editorMax.y - editorMin.y) / 4.0f); + + // A bit of a hack to prevent editor from dragging and selecting while dragging an input field + // It still draws a dot where the Selection Action should be, but it's not too bad + if (m_State->DraggingInputField || draggingCanvasTitlebar) + { + auto* editor = GetEditorContext(); + if (auto* action = editor->GetCurrentAction()) + { + if (auto* dragAction = action->AsDrag()) + dragAction->m_Clear = true; + if (auto* selectAction = action->AsSelect()) + selectAction->m_IsActive = false; + } + } + return { editorMin, editorMax }; + } + + + void NodeGraphEditorBase::DrawNodes(PinPropertyContext& pinContext) + { + utils::BlueprintNodeBuilder builder(UI::GetTextureID(m_NodeBuilderTexture), m_NodeBuilderTexture->GetWidth(), m_NodeBuilderTexture->GetHeight()); + + // All nodes other than comments must be drawn first, + // and then all comment nodes drawn at the end. + // and then all comment nfaodes drawn at the end. + // This is because comment nodes can act to group together + // other nodes (and hence all other nodes must be drawn first) + for (auto& node : GetModel()->GetNodes()) + { + // When we do a copy/paste the pasted nodes have no position in the editor. + // In this case, set the editor node pos from the position saved in the node state + if((ed::GetNodePosition(ed::NodeId(node->ID)) == ImVec2(FLT_MAX, FLT_MAX)) && (!node->State.empty())) + ed::SetNodePosition(ed::NodeId(node->ID), node->GetPosition()); + + if (node->Type != NodeType::Comment) + DrawNode(node, builder, pinContext); + } + + for (auto& node : GetModel()->GetNodes()) + { + if (node->Type == NodeType::Comment) + DrawCommentNode(node); + } + } + + + void NodeGraphEditorBase::DrawLinks() + { + // Links + for (auto& link : GetModel()->GetLinks()) + ed::Link(ed::LinkId(link.ID), ed::PinId(link.StartPinID), ed::PinId(link.EndPinID), link.Color, 2.0f); + + m_AcceptingLinkPins = { nullptr, nullptr }; + + // Draw dragging Link + if (!m_State->CreateNewNode) + { + const ImColor draggedLinkColour = ImColor(255, 255, 255); + const float lineThickness = 2.0f; + if (ed::BeginCreate(draggedLinkColour, lineThickness)) + { + auto showLabel = [](const char* label, ImColor color) + { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); + auto size = ImGui::CalcTextSize(label); + + auto padding = ImGui::GetStyle().FramePadding; + auto spacing = ImGui::GetStyle().ItemSpacing; + + ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(spacing.x, -spacing.y)); + + auto rectMin = ImGui::GetCursorScreenPos() - padding; + auto rectMax = ImGui::GetCursorScreenPos() + size + padding; + + auto drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(rectMin, rectMax, color, size.y * 0.15f); + ImGui::TextUnformatted(label); + }; + + ed::PinId startPinId = 0, endPinId = 0; + if (ed::QueryNewLink(&startPinId, &endPinId)) + { + auto startPin = GetModel()->FindPin(startPinId.Get()); + auto endPin = GetModel()->FindPin(endPinId.Get()); + + m_State->NewLinkPin = startPin ? startPin : endPin; + + if (startPin->Kind == PinKind::Input) + { + std::swap(startPin, endPin); + std::swap(startPinId, endPinId); + } + + if (startPin && endPin && (startPin != endPin)) + { + NodeEditorModel::LinkQueryResult canConnect = GetModel()->CanCreateLink(startPin, endPin); + if (canConnect) + { + m_AcceptingLinkPins.first = startPin; + m_AcceptingLinkPins.second = endPin; + showLabel("+ Create Link", ImColor(32, 45, 32, 180)); + + // Set link color when attacking to a compatible pin + const ImColor linkColor = GetIconColor(startPin->GetType()); + + if (ed::AcceptNewItem(linkColor, 4.0f)) + { + GetModel()->CreateLink(startPin, endPin); + } + } + else + { + switch (canConnect.Reason) + { + case NodeEditorModel::LinkQueryResult::IncompatibleStorageKind: + case NodeEditorModel::LinkQueryResult::IncompatiblePinKind: + { + showLabel("x Incompatible Pin Kind", ImColor(45, 32, 32, 180)); + ed::RejectNewItem(ImColor(255, 0, 0), 2.0f); + break; + } + case NodeEditorModel::LinkQueryResult::IncompatibleType: + { + showLabel("x Incompatible Pin Type", ImColor(45, 32, 32, 180)); + ed::RejectNewItem(ImColor(255, 128, 128), 1.0f); + break; + } + case NodeEditorModel::LinkQueryResult::SamePin: + case NodeEditorModel::LinkQueryResult::SameNode: + { + showLabel("x Cannot connect to self", ImColor(45, 32, 32, 180)); + ed::RejectNewItem(ImColor(255, 0, 0), 1.0f); + break; + } + case NodeEditorModel::LinkQueryResult::CausesLoop: + { + showLabel("x Connection causes loop", ImColor(45, 32, 32, 180)); + ed::RejectNewItem(ImColor(255, 0, 0), 1.0f); + break; + } + case NodeEditorModel::LinkQueryResult::InvalidStartPin: + case NodeEditorModel::LinkQueryResult::InvalidEndPin: + default: + break; + } + } + } + } + + ed::PinId pinId = 0; + if (ed::QueryNewNode(&pinId)) + { + m_State->NewLinkPin = GetModel()->FindPin(pinId.Get()); + if (m_State->NewLinkPin) + showLabel("+ Create Node", ImColor(32, 45, 32, 180)); + + const ImColor draggedLinkColour = m_State->NewLinkPin ? GetIconColor(m_State->NewLinkPin->GetType()) : ImColor(255, 255, 255); + const float lineThickness = 2.0f; + + if (ed::AcceptNewItem(draggedLinkColour, lineThickness)) + { + m_State->CreateNewNode = true; + m_State->NewNodeLinkPinId = pinId.Get(); + m_State->NewLinkPin = nullptr; + + ed::Suspend(); + ImGui::OpenPopup("Create New Node"); + m_State->NewNodePopupOpening = true; + ed::Resume(); + } + } + } + else + m_State->NewLinkPin = nullptr; + + ed::EndCreate(); + } + + // Alt + Click to remove links connected to clicked pin + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::GetIO().KeyAlt) + { + auto hoveredPinId = (UUID)ed::GetHoveredPin().Get(); + if (hoveredPinId) + { + auto* model = GetModel(); + std::vector links = model->GetAllLinkIDsConnectedToPin(hoveredPinId); + model->RemoveLinks(links); + } + } + } + + + void NodeGraphEditorBase::DrawNodeHeader(Node* node, NodeBuilder& builder) + { + bool hasOutputDelegates = false; + for (auto& output : node->Outputs) + if (output->IsType(EPinType::DelegatePin)) + hasOutputDelegates = true; + + builder.Header(node->Color); + { + ImGui::Spring(0); + UI::ScopedColour headerTextColor(ImGuiCol_Text, IM_COL32(210, 210, 210, 255)); + + if (node->Type == NodeType::Subroutine) + { + static bool wasActive = false; + + const auto inputTextFlags = ImGuiInputTextFlags_AutoSelectAll; + + // Update our node size if backend node size has been changed + const ImVec2 nodeSize = ed::GetNodeSize((uint64_t)node->ID); + float availableWidth = 0.0f; + if (nodeSize == ImVec2(0.0f, 0.0f)) + { + availableWidth = node->Size.x; + } + else + { + availableWidth = nodeSize.x; + if (nodeSize != node->Size) + node->Size = nodeSize; + } + + ImVec2 textSize = ImGui::CalcTextSize((node->Name + "00").c_str()); + textSize.x = std::min(textSize.x, availableWidth - 16.0f); + + ImGui::PushItemWidth(textSize.x); + { + UI::ScopedColourStack stack(ImGuiCol_FrameBg, ImColor(0, 0, 0, 0), ImGuiCol_Border, IM_COL32_DISABLE); + ImGui::InputText("##edit", &node->Description, inputTextFlags); + DrawItemActivityOutline(UI::OutlineFlags_NoOutlineInactive); + } + ImGui::PopItemWidth(); + wasActive = DeactivateInputIfDragging(wasActive); + } + else + { + ImGui::TextUnformatted(node->Name.c_str()); + } + + if (m_ShowNodeIDs) + ImGui::TextUnformatted(("(" + std::to_string(node->ID) + ")").c_str()); + if (m_ShowSortIndices) + ImGui::TextUnformatted(("(sort index: " + std::to_string(node->SortIndex) + ")").c_str()); + } + ImGui::Spring(1); + + const float nodeHeaderHeight = 18.0f; + ImGui::Dummy(ImVec2(0, nodeHeaderHeight)); + + // Delegate pin + if (hasOutputDelegates) + { + ImGui::BeginVertical("delegates", ImVec2(0, nodeHeaderHeight)); + ImGui::Spring(1, 0); + for (auto& output : node->Outputs) + { + if (output->IsType(EPinType::DelegatePin)) + continue; + + auto alpha = ImGui::GetStyle().Alpha; + if (m_State->NewLinkPin && !GetModel()->CanCreateLink(m_State->NewLinkPin, output) && output != m_State->NewLinkPin) + { + alpha = alpha * (48.0f / 255.0f); + } + + ed::BeginPin(ed::PinId(output->ID), ed::PinKind::Output); + ed::PinPivotAlignment(ImVec2(1.0f, 0.5f)); + ed::PinPivotSize(ImVec2(0, 0)); + ImGui::BeginHorizontal((int)output->ID); + UI::ScopedStyle alphaStyle(ImGuiStyleVar_Alpha, alpha); + if (!output->Name.empty()) + { + ImGui::TextUnformatted(output->Name.c_str()); + ImGui::Spring(0); + } + DrawPinIcon(output, GetModel()->IsPinLinked(output->ID), (int)(alpha * 255)); + ImGui::Spring(0, ImGui::GetStyle().ItemSpacing.x / 2); + ImGui::EndHorizontal(); + ed::EndPin(); + } + ImGui::Spring(1, 0); + ImGui::EndVertical(); + ImGui::Spring(0, ImGui::GetStyle().ItemSpacing.x / 2); + } + else + { + ImGui::Spring(0); + } + + builder.EndHeader(); + } + + + void NodeGraphEditorBase::DrawNodeMiddle(Node* node, NodeBuilder& builder) + { + if (node->Type == NodeType::Simple) + { + builder.Middle(); + + ImGui::Spring(1, 0); + UI::ScopedColour textColour(ImGuiCol_Text, IM_COL32(210, 210, 210, 255)); + + // TODO: handle simple nodes display icon properly + if (const char* displayIcon = GetIconForSimpleNode(node->Name)) + { + UI::ScopedColour textColour(ImGuiCol_Text, Colors::Theme::text); + UI::ScopedFont largeFont(ImGui::GetIO().Fonts->Fonts[1]); + ImGui::TextUnformatted(displayIcon); + } + else + ImGui::TextUnformatted(node->Name.c_str()); + + if (m_ShowNodeIDs) + ImGui::TextUnformatted(("(" + std::to_string(node->ID) + ")").c_str()); + + ImGui::Spring(1, 0); + } + + if (node->Type == NodeType::Subroutine) + { + builder.Middle(); + UI::ScopedColour textColour(ImGuiCol_Text, IM_COL32(210, 210, 210, 255)); + UI::ScopedStyle framePadding(ImGuiStyleVar_FramePadding, ImVec2(16, 8)); + UI::ScopedStyle frameRounding(ImGuiStyleVar_FrameRounding, 4.0f); + if (ImGui::Button("Edit...")) + { + m_State->EditNodeId = node->ID; + } + } + + } + + void NodeGraphEditorBase::DrawNodeInputs(Node* node, NodeBuilder& builder, PinPropertyContext& pinContext) + { + for (auto& input : node->Inputs) + { + bool pinValueChanged = false; + + auto alpha = ImGui::GetStyle().Alpha; + if (m_State->NewLinkPin && !GetModel()->CanCreateLink(m_State->NewLinkPin, input) && input != m_State->NewLinkPin) + { + alpha = alpha * (48.0f / 255.0f); + } + + builder.Input(ed::PinId(input->ID)); + UI::ScopedStyle alphaStyle(ImGuiStyleVar_Alpha, alpha); + DrawPinIcon(input, GetModel()->IsPinLinked(input->ID), (int)(alpha * 255)); + ImGui::Spring(0); + + // Draw input pin name + if ((node->Type != NodeType::Simple) && !input->Name.empty()) + { + ImGui::TextUnformatted(input->Name.c_str()); + ImGui::Spring(0); + } + + // TODO: implementation might want to draw property edit for linked pins as well(?) + if (input->Storage != StorageKind::Array && !GetModel()->IsPinLinked(input->ID)) + { + UI::ScopedStyle frameRounding(ImGuiStyleVar_FrameRounding, 2.5f); + + // TODO: handle implementation specific types + PinPropertyContext context{ input, GetModel(), false, false }; + pinValueChanged = DrawPinPropertyEdit(context); + + if (context.OpenAssetPopup || context.OpenEnumPopup) + pinContext = context; + + static bool wasActive = false; + wasActive = DeactivateInputIfDragging(wasActive); //? this might not work, if it requires unique bool for each property type, or if property edit was not even drawn + + ImGui::Spring(0); + } + + if (pinValueChanged && GetModel()->onPinValueChanged) + { + GetModel()->onPinValueChanged(node->ID, input->ID); + } + + builder.EndInput(); + } + } + + + void NodeGraphEditorBase::DrawNodeOutputs(Node* node, NodeBuilder& builder) + { + for (auto& output : node->Outputs) + { + if ((node->Type != NodeType::Simple) && output->IsType(EPinType::DelegatePin)) + continue; + + auto alpha = ImGui::GetStyle().Alpha; + if (m_State->NewLinkPin && !GetModel()->CanCreateLink(m_State->NewLinkPin, output) && output != m_State->NewLinkPin) + { + alpha = alpha * (48.0f / 255.0f); + } + + UI::ScopedStyle alphaStyleOverride(ImGuiStyleVar_Alpha, alpha); + + builder.Output(ed::PinId(output->ID)); + + // Draw output pin name + if ((node->Type != NodeType::Simple) && !output->Name.empty() && output->Name != "Message") + { + ImGui::Spring(0); + ImGui::TextUnformatted(output->Name.c_str()); + } + ImGui::Spring(0); + DrawPinIcon(output, GetModel()->IsPinLinked(output->ID), (int)(alpha * 255)); + builder.EndOutput(); + + // TODO: implementation might want to draw property edit for outputs as well + } + } + + + void NodeGraphEditorBase::DrawNode(Node* node, NodeBuilder& builder, PinPropertyContext& pinContext) + { + SE_CORE_ASSERT(node->Type != NodeType::Comment); + builder.Begin(ed::NodeId(node->ID)); + + if (node->Type != NodeType::Simple) + { + DrawNodeHeader(node, builder); + } + DrawNodeInputs(node, builder, pinContext); + DrawNodeMiddle(node, builder); + DrawNodeOutputs(node, builder); + + builder.End(); + + int radius = EditorResources::ShadowTexture->GetHeight();// *1.4; + UI::DrawShadow(EditorResources::ShadowTexture, radius); + } + + + void NodeGraphEditorBase::DrawCommentNode(Node* node) + { + SE_CORE_ASSERT(node->Type == NodeType::Comment); + + const float commentAlpha = 0.75f; + + ed::PushStyleColor(ed::StyleColor_NodeBg, node->Color); + ed::PushStyleColor(ed::StyleColor_NodeBorder, ImColor(255, 255, 255, 24)); + ImGui::PushStyleColor(ImGuiCol_Border, IM_COL32_DISABLE); + ed::BeginNode(ed::NodeId(node->ID)); + { + UI::ScopedStyle nodeStyle(ImGuiStyleVar_Alpha, commentAlpha); + + UI::ScopedID nodeID((int)node->ID); + ImGui::BeginVertical("content"); + ImGui::BeginHorizontal("horizontal"); + { + UI::ShiftCursorX(6.0f); + UI::ScopedColour frameColour(ImGuiCol_FrameBg, IM_COL32(255, 255, 255, 0)); + UI::ScopedColour textColour(ImGuiCol_Text, IM_COL32(220, 220, 220, 255)); + UI::ScopedFont largeFont(ImGui::GetIO().Fonts->Fonts[1]); + + static bool wasActive = false; + + const auto inputTextFlags = ImGuiInputTextFlags_AutoSelectAll; + + // Update our node size if backend node size has been changed + const ImVec2 nodeSize = ed::GetNodeSize((uint64_t)node->ID); + float availableWidth = 0.0f; + if (nodeSize == ImVec2(0.0f, 0.0f)) + { + availableWidth = node->Size.x; + } + else + { + availableWidth = nodeSize.x; + + if (nodeSize != node->Size) + node->Size = nodeSize; + } + + ImVec2 textSize = ImGui::CalcTextSize((node->Name + "00").c_str()); + textSize.x = std::min(textSize.x, availableWidth - 16.0f); + + ImGui::PushItemWidth(textSize.x); + //ImGui::PushTextWrapPos(availableWidth); //? doesn't work + { + UI::ScopedStyle alpha(ImGuiStyleVar_Alpha, 1.0f); + + // Draw text shadow + { + UI::ScopedColour textShadow(ImGuiCol_Text, IM_COL32_BLACK); + const auto pos = ImGui::GetCursorPos(); + UI::ShiftCursor(4.0f, 4.0f); + ImGui::TextUnformatted(node->Name.c_str()); + ImGui::SetCursorPos(pos); + } + + ImGui::InputText("##edit", &node->Name, inputTextFlags); + DrawItemActivityOutline(UI::OutlineFlags_NoOutlineInactive); + } + //ImGui::PopTextWrapPos(); + ImGui::PopItemWidth(); + + wasActive = DeactivateInputIfDragging(wasActive); + } + ImGui::EndHorizontal(); + ed::Group(node->Size); + ImGui::EndVertical(); + } + ed::EndNode(); + ImGui::PopStyleColor(); // Border + ed::PopStyleColor(2); // StyleColor_NodeBg, StyleColor_NodeBorder + + // Popup hint when zoomed out + if (ed::BeginGroupHint(ed::NodeId(node->ID))) + { + auto bgAlpha = static_cast(ImGui::GetStyle().Alpha * 255); + + auto min = ed::GetGroupMin(); + + ImGui::SetCursorScreenPos(min - ImVec2(-8, ImGui::GetTextLineHeightWithSpacing() + 4)); + ImGui::BeginGroup(); + ImGui::TextUnformatted(node->Name.c_str()); + ImGui::EndGroup(); + + auto drawList = ed::GetHintBackgroundDrawList(); + + auto hintBounds = UI::GetItemRect(); + auto hintFrameBounds = UI::RectExpanded(hintBounds, 8, 4); + + drawList->AddRectFilled( + hintFrameBounds.GetTL(), + hintFrameBounds.GetBR(), + IM_COL32(255, 255, 255, 64 * bgAlpha / 255), 1.0f); + + drawList->AddRect( + hintFrameBounds.GetTL(), + hintFrameBounds.GetBR(), + IM_COL32(255, 255, 255, 128 * bgAlpha / 255), 1.0f); + } + ed::EndGroupHint(); + } + + + void NodeGraphEditorBase::DrawPinIcon(const Pin* pin, bool connected, int alpha) + { + const int pinIconSize = GetPinIconSize(); + + //IconType iconType; + ImColor color = GetIconColor(pin->GetType()); + color.Value.w = alpha / 255.0f; + + // Draw a highlight if hovering over this pin or its label + if (ed::PinId(pin->ID) == ed::GetHoveredPin()) + { + auto* drawList = ImGui::GetWindowDrawList(); + auto size = ImVec2(static_cast(pinIconSize), static_cast(pinIconSize)); + auto cursorPos = ImGui::GetCursorScreenPos(); + const auto outline_scale = size.x / 24.0f; + const auto extra_segments = static_cast(2 * outline_scale); // for full circle + const auto radius = 0.5f * size.x / 2.0f - 0.5f; + const auto centre = ImVec2(cursorPos.x + size.x / 2.0f, cursorPos.y + size.y / 2.0f); + drawList->AddCircleFilled(centre, 0.5f * size.x, IM_COL32(255, 255, 255, 30), 12 + extra_segments); + } + + // If this pin accepting a link, draw it as connected + bool acceptingLink = IsPinAcceptingLink(pin); + + if (pin->Storage == StorageKind::Array) + ax::Widgets::IconGrid(ImVec2(static_cast(pinIconSize), static_cast(pinIconSize)), connected || acceptingLink, color, ImColor(32, 32, 32, alpha)); + else + { + Ref image; + + if (pin->GetType() == EPinType::Flow) + { + image = connected || acceptingLink ? EditorResources::PinFlowConnectIcon : EditorResources::PinFlowDisconnectIcon; + } + else + { + image = connected || acceptingLink ? EditorResources::PinValueConnectIcon : EditorResources::PinValueDisconnectIcon; + } + + const auto iconWidth = image->GetWidth(); + const auto iconHeight = image->GetHeight(); + ax::Widgets::ImageIcon(ImVec2(static_cast(iconWidth), static_cast(iconHeight)), UI::GetTextureID(image), connected, (float)pinIconSize, color, ImColor(32, 32, 32, alpha)); + } + } + + + void NodeGraphEditorBase::CheckContextMenus() + { + // Check whether any context menus should be shown. + + if (ed::ShowNodeContextMenu(&m_State->ContextNodeId)) + { + if (!ed::IsNodeSelected(m_State->ContextNodeId)) + { + ed::SelectNode(m_State->ContextNodeId, ImGui::IsKeyDown(ImGuiKey_LeftCtrl)); + } + ImGui::OpenPopup("Node Context Menu"); + } + else if (ed::ShowPinContextMenu(&m_State->ContextPinId)) + { + ImGui::OpenPopup("Pin Context Menu"); + } + else if (ed::ShowLinkContextMenu(&m_State->ContextLinkId)) + { + if (!ed::IsLinkSelected(m_State->ContextLinkId)) + { + ed::SelectLink(m_State->ContextLinkId, ImGui::IsKeyDown(ImGuiKey_LeftCtrl)); + } + ImGui::OpenPopup("Link Context Menu"); + } + else if (ed::ShowBackgroundContextMenu()) + { + ImGui::OpenPopup("Create New Node"); + m_State->NewNodePopupOpening = true; + m_State->NewNodeLinkPinId = 0; + } + } + + + void NodeGraphEditorBase::DrawNodeContextMenu(Node* node) + { + ImGui::TextUnformatted("Node Context Menu"); + ImGui::Separator(); + if (node) + { + ImGui::Text("ID: %s", std::to_string(node->ID).c_str()); + ImGui::Text("Sort index: %d", node->SortIndex); + ImGui::Text("Inputs: %d", (int)node->Inputs.size()); + ImGui::Text("Outputs: %d", (int)node->Outputs.size()); + } + else + ImGui::Text("Unknown node: %p", m_State->ContextNodeId.AsPointer()); + ImGui::Separator(); + if (ImGui::MenuItem("Delete")) + ed::DeleteNode(m_State->ContextNodeId); + } + + + void NodeGraphEditorBase::DrawPinContextMenu(Pin* pin) + { + ImGui::TextUnformatted("Pin Context Menu"); + ImGui::Separator(); + if (pin) + { + ImGui::Text("ID: %s", std::to_string(pin->ID).c_str()); + if (pin->NodeID) + { + ImGui::Text("Node: %s", std::to_string(pin->NodeID).c_str()); + } + else + ImGui::Text("Node: %s", ""); + + ImGui::Text("Value type: %s", Utils::GetTypeName(pin->Value).c_str()); + } + else + ImGui::Text("Unknown pin: %s", m_State->ContextPinId.AsPointer()); + } + + + void NodeGraphEditorBase::DrawLinkContextMenu(Link* link) + { + ImGui::TextUnformatted("Link Context Menu"); + ImGui::Separator(); + if (link) + { + ImGui::Text("ID: %s", std::to_string(link->ID).c_str()); + ImGui::Text("From: %s", std::to_string(link->StartPinID).c_str()); + ImGui::Text("To: %s", std::to_string(link->EndPinID).c_str()); + } + else + ImGui::Text("Unknown link: %p", m_State->ContextLinkId.AsPointer()); + ImGui::Separator(); + if (ImGui::MenuItem("Delete")) + ed::DeleteLink(m_State->ContextLinkId); + } + + + void NodeGraphEditorBase::DrawBackgroundContextMenu(bool& isNewNodePoppedUp, ImVec2& newNodePostion) + { + isNewNodePoppedUp = true; + newNodePostion = ed::ScreenToCanvas(ImGui::GetMousePosOnOpeningCurrentPopup()); + Node* node = nullptr; + + // Search widget + static bool grabFocus = true; + static std::string searchString; + if (ImGui::GetCurrentWindow()->Appearing) + { + grabFocus = true; + searchString.clear(); + } + UI::ShiftCursorX(4.0f); + UI::ShiftCursorY(2.0f); + UI::Widgets::SearchWidget(searchString, "Search nodes...", &grabFocus); + const bool searching = !searchString.empty(); + { + UI::ScopedColourStack headerColours( + ImGuiCol_Header, IM_COL32(255, 255, 255, 0), + ImGuiCol_HeaderActive, IM_COL32(45, 46, 51, 255), + ImGuiCol_HeaderHovered, IM_COL32(0, 0, 0, 80) + ); + + //UI::ScopedColour scrollList(ImGuiCol_ChildBg, IM_COL32(255, 255, 255, 0)); + //UI::ScopedStyle scrollListStyle(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + UI::ShiftCursorY(GImGui->Style.WindowPadding.y + 2.0f); + if (ImGui::BeginChild("##new_node_scroll_list", ImVec2(0.0f, 350.0f))) + { + // If subclass graph editor has any custom nodes, + // the popup items for them can be added here + if (onNodeListPopup) + node = onNodeListPopup(searching, searchString); + + const auto& nodeRegistry = GetModel()->GetNodeTypes(); + for (const auto& [categoryName, category] : nodeRegistry) + { + // Can use this instead of the collapsing header + //UI::PopupMenuHeader(categoryName, true, false); + if (searching) + ImGui::SetNextItemOpen(true); + + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(140, 140, 140, 255)); + if (UI::ContextMenuHeader(categoryName.c_str())) + { + ImGui::PopStyleColor(); // header Text + //ImGui::Separator(); + + ImGui::Indent(); + //------------------------------------------------- + if (searching) + { + for (const auto& [nodeType, spawnFunction] : category) + { + if (UI::IsMatchingSearch(categoryName, searchString) + || UI::IsMatchingSearch(nodeType, searchString)) + { + if (ImGui::MenuItem(nodeType.c_str())) + node = GetModel()->CreateNode(categoryName, nodeType); + } + } + } + else + { + for (const auto& [nodeType, spawnFunction] : category) + { + if (ImGui::MenuItem(nodeType.c_str())) + node = GetModel()->CreateNode(categoryName, nodeType); + } + } + + if (nodeRegistry.find(categoryName) != nodeRegistry.end()) + ImGui::Spacing(); + ImGui::Unindent(); + } + else + { + ImGui::PopStyleColor(); // header Text + } + } + } + ImGui::EndChild(); + } + + if (node) + { + m_State->CreateNewNode = false; + ed::SetNodePosition(ed::NodeId(node->ID), newNodePostion); + if (auto* startPin = GetModel()->FindPin(m_State->NewNodeLinkPinId)) + { + auto& pins = startPin->Kind == PinKind::Input ? node->Outputs : node->Inputs; + SE_CORE_ASSERT(!pins.empty()); + for (auto& pin : pins) + { + if (GetModel()->CanCreateLink(startPin, pin)) + { + auto endPin = pin; + if (startPin->Kind == PinKind::Input) + std::swap(startPin, endPin); + + GetModel()->CreateLink(startPin, endPin); + break; + } + } + } + + if (onNewNodeFromPopup) + onNewNodeFromPopup(node); + + ImGui::CloseCurrentPopup(); + } + m_State->NewNodePopupOpening = false; + } + + + void NodeGraphEditorBase::DrawDeferredComboBoxes(PinPropertyContext& pinContext) + { + ed::Suspend(); + { + /// Asset pin search popup + { + if (pinContext.OpenAssetPopup) + ImGui::OpenPopup("AssetSearch"); + + // TODO: get item rect translated from canvas to screen coords and position popup properly + + // TODO: AssetSearchList should be combined with the button that opens it up. + // At the moment they are separate. + bool clear = false; + if (UI::Widgets::AssetSearchPopup("AssetSearch", NodeEditorDraw::s_SelectAssetContext.AssetType, NodeEditorDraw::s_SelectAssetContext.SelectedAssetHandle, &clear, "Search Asset", ImVec2{ 250.0f, 300.0f })) + { + if (auto pin = GetModel()->FindPin(NodeEditorDraw::s_SelectAssetContext.PinID)) + { + pin->Value = Utils::CreateAssetHandleValue(NodeEditorDraw::s_SelectAssetContext.AssetType, clear? AssetHandle(0) : NodeEditorDraw::s_SelectAssetContext.SelectedAssetHandle); + if (const auto& onPinValueChanged = GetModel()->onPinValueChanged) + { + onPinValueChanged(pin->NodeID, pin->ID); + } + } + + NodeEditorDraw::s_SelectAssetContext = {}; + } + } + + /// Enum combo box + { + if (pinContext.OpenEnumPopup && GetModel()->FindPin(s_SelectedEnumContext.PinID)) + ImGui::OpenPopup("##EnumPopup"); + + if (auto pin = GetModel()->FindPin(s_SelectedEnumContext.PinID)) + { + bool changed = false; + SE_CORE_ASSERT(pin->EnumTokens.has_value() && pin->EnumTokens.value() != nullptr); + const auto& tokens = **pin->EnumTokens; + + ImGui::SetNextWindowSize({ NodeEditorDraw::GetBestWidthForEnumCombo(tokens), 0.0f }); + if (UI::BeginPopup("##EnumPopup", ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) + { + for (int i = 0; i < tokens.size(); ++i) + { + ImGui::PushID(i); + const bool itemSelected = (i == s_SelectedEnumContext.SelectedValue); + if (ImGui::Selectable(tokens[i].name.data(), itemSelected)) + { + changed = true; + s_SelectedEnumContext.SelectedValue = i; + } + if (itemSelected) + ImGui::SetItemDefaultFocus(); + ImGui::PopID(); + } + UI::EndPopup(); + } + + if (changed) + { + pin->Value = s_SelectedEnumContext.ConstructValue(s_SelectedEnumContext.SelectedValue); + if (GetModel()->onPinValueChanged) + GetModel()->onPinValueChanged(pin->NodeID, pin->ID); + } + } + } + } + ed::Resume(); + } + + + //============================================================================================= + /// IO Node Graph Editor + //============================================================================================= + + + IONodeGraphEditor::SelectedItem::operator bool() const + { + return (!Name.empty() || ID) && SelectionType != ESelectionType::Invalid; + } + + bool IONodeGraphEditor::SelectedItem::IsSelected(EPropertyType type, std::string_view name) const + { + return SelectionType == FromPropertyType(type) && Name == name; + } + + void IONodeGraphEditor::SelectedItem::Select(EPropertyType type, std::string_view name) + { + SelectionType = FromPropertyType(type); + Name = name; + ID = 0; + ed::ClearSelection(); + } + + void IONodeGraphEditor::SelectedItem::SelectNode(UUID id) + { + SelectionType = ESelectionType::Node; + ID = id; + Name.clear(); + } + + void IONodeGraphEditor::SelectedItem::Clear() + { + SelectionType = ESelectionType::Invalid; Name.clear(); ID = 0; + } + + EPropertyType IONodeGraphEditor::SelectedItem::GetPropertyType() const + { + switch (SelectionType) + { + case ESelectionType::Input: return EPropertyType::Input; + case ESelectionType::Output: return EPropertyType::Output; + case ESelectionType::LocalVariable: return EPropertyType::LocalVariable; + case ESelectionType::Node: + case ESelectionType::Invalid: + default: return EPropertyType::Invalid; + } + } + + IONodeGraphEditor::ESelectionType IONodeGraphEditor::SelectedItem::FromPropertyType(EPropertyType type) const + { + switch (type) + { + case EPropertyType::Input: return ESelectionType::Input; + case EPropertyType::Output: return ESelectionType::Output; + case EPropertyType::LocalVariable: return ESelectionType::LocalVariable; + case EPropertyType::Invalid: + default: return ESelectionType::Invalid; + } + } + + + IONodeGraphEditor::IONodeGraphEditor(const char* id) : NodeGraphEditorBase(id) + { + m_SelectedItem = CreateScope(); + + m_ToolbarName = "Toolbar##" + m_Id; + m_GraphInputsName = "Graph Inputs##" + m_Id; + m_GraphDetailsName = "Graph Details##" + m_Id; + m_DetailsName = "Details##" + m_Id; + + onNodeListPopup = [&](bool searching, std::string_view searchedString) + { + Node* newNode = nullptr; + + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 170, 170, 255)); + if (ImGui::CollapsingHeader("Graph Inputs", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PopStyleColor(); // header Text + ImGui::Separator(); + + ImGui::Indent(); + //------------------------------------------------- + if (searching) + { + for (const auto& graphInput : m_Model->GetInputs().GetNames()) + { + if (UI::IsMatchingSearch("Graph Inputs", searchedString) + || UI::IsMatchingSearch(graphInput, searchedString)) + { + if (ImGui::MenuItem(choc::text::replace(graphInput, "_", " ").c_str())) // TODO: SpawnGraphOutputNode + newNode = m_Model->SpawnGraphInputNode(graphInput); + } + } + } + else + { + for (const auto& graphInput : m_Model->GetInputs().GetNames()) + { + if (ImGui::MenuItem(choc::text::replace(graphInput, "_", " ").c_str())) // TODO: SpawnGraphOutputNode + newNode = m_Model->SpawnGraphInputNode(graphInput); + } + } + + ImGui::Unindent(); + } + else + { + ImGui::PopStyleColor(); // header Text + } + + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(140, 140, 140, 255)); + if (UI::ContextMenuHeader("Local Variables", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PopStyleColor(); // header Text + + ImGui::Indent(); + //------------------------------------------------- + if (searching) + { + for (const auto& localVariable : m_Model->GetLocalVariables().GetNames()) + { + const std::string setter = localVariable + " Set"; + const std::string getter = localVariable + " Get"; + + if (UI::IsMatchingSearch("Local Variables", searchedString) + || UI::IsMatchingSearch(setter, searchedString) + || UI::IsMatchingSearch(getter, searchedString)) + { + if (ImGui::MenuItem(choc::text::replace(setter, "_", " ").c_str())) + newNode = m_Model->SpawnLocalVariableNode(localVariable, false); + + if (ImGui::MenuItem(choc::text::replace(getter, "_", " ").c_str())) + newNode = m_Model->SpawnLocalVariableNode(localVariable, true); + } + } + } + else + { + for (const auto& localVariable : m_Model->GetLocalVariables().GetNames()) + { + const std::string setter = localVariable + " Set"; + const std::string getter = localVariable + " Get"; + + if (ImGui::MenuItem(choc::text::replace(setter, "_", " ").c_str())) + newNode = m_Model->SpawnLocalVariableNode(localVariable, false); + + if (ImGui::MenuItem(choc::text::replace(getter, "_", " ").c_str())) + newNode = m_Model->SpawnLocalVariableNode(localVariable, true); + } + } + + ImGui::Unindent(); + } + else + { + ImGui::PopStyleColor(); // header Text + } + + return newNode; + }; + } + + + IONodeGraphEditor::~IONodeGraphEditor() = default; + + + void IONodeGraphEditor::OnRender() + { + ImGui::SetNextWindowClass(GetWindowClass()); + + if (ImGui::Begin(m_ToolbarName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse)) + { + //EnsureWindowIsDocked(ImGui::GetCurrentWindow()); + DrawToolbar(); + } + ImGui::End(); // Toolbar + + ImGui::SetNextWindowClass(GetWindowClass()); + if (ImGui::Begin(m_GraphDetailsName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollWithMouse)) + { + //EnsureWindowIsDocked(ImGui::GetCurrentWindow()); + DrawDetailsPanel(); + } + ImGui::End(); // Graph details + + ImGui::SetNextWindowClass(GetWindowClass()); + if (ImGui::Begin(m_GraphInputsName.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollWithMouse)) + { + //EnsureWindowIsDocked(ImGui::GetCurrentWindow()); + DrawGraphIO(); + + if (ed::HasSelectionChanged()) + { + std::vector selectedNodes = GetSelectedNodeIDs(); + if (selectedNodes.size() == 1) + { + m_SelectedItem->SelectNode(selectedNodes.back()); + } + else if (m_SelectedItem->SelectionType == ESelectionType::Node) + { + m_SelectedItem->Clear(); + } + } + + ImGui::SetNextWindowClass(GetWindowClass()); + if (ImGui::Begin(m_DetailsName.c_str())) + { + DrawSelectedDetails(); + } + ImGui::End(); // Selected item details + } + ImGui::End(); // Graph Inputs + } + + + ImGuiWindowFlags IONodeGraphEditor::GetWindowFlags() + { + return ImGuiWindowFlags_NoCollapse; + } + + + void IONodeGraphEditor::OnWindowStylePush() + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(2.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + + /*if (ImGui::Begin("Style Editor")) + ImGui::ShowStyleEditor(); + + ImGui::End();*/ + } + + + void IONodeGraphEditor::OnWindowStylePop() + { + ImGui::PopStyleVar(2); // ImGuiStyleVar_WindowPadding, ImGuiStyleVar_FrameBorderSize + } + + + const char* IONodeGraphEditor::GetIconForSimpleNode(const std::string& simpleNodeIdentifier) const + { + if (choc::text::contains(simpleNodeIdentifier, "Add")) return "+"; + if (choc::text::contains(simpleNodeIdentifier, "Subtract")) return "-"; + if (choc::text::contains(simpleNodeIdentifier, "Mult")) return "x"; + if (choc::text::contains(simpleNodeIdentifier, "Divide")) return "/"; + if (choc::text::contains(simpleNodeIdentifier, "Modulo")) return "%"; + if (choc::text::contains(simpleNodeIdentifier, "Pow")) return "^"; + if (choc::text::contains(simpleNodeIdentifier, "Log")) return "log"; + if (choc::text::contains(simpleNodeIdentifier, "Less Equal")) return "<="; + if (choc::text::contains(simpleNodeIdentifier, "Not Equal")) return "!="; + if (choc::text::contains(simpleNodeIdentifier, "Greater Equal")) return ">="; + if (choc::text::contains(simpleNodeIdentifier, "Equal")) return "=="; + if (choc::text::contains(simpleNodeIdentifier, "Less")) return "<"; + if (choc::text::contains(simpleNodeIdentifier, "Greater")) return ">"; + + else return nullptr; + } + + + void IONodeGraphEditor::SelectNode(UUID id) + { + m_SelectedItem->SelectNode(id); + } + + + bool IONodeGraphEditor::IsAnyNodeSelected() const + { + return m_SelectedItem->SelectionType == ESelectionType::Node; + } + + + void IONodeGraphEditor::ClearSelection() + { + m_SelectedItem->Clear(); + } + + + void IONodeGraphEditor::DrawToolbar() + { + auto* drawList = ImGui::GetWindowDrawList(); + + const float spacing = 16.0f; + + ImGui::BeginHorizontal("ToolbarHorizontalLayout", ImGui::GetContentRegionAvail()); + ImGui::Spring(0.0f, spacing); + + // Compile Button + { + float compileIconWidth = EditorResources::CompileIcon->GetWidth() * 0.9f; + float compileIconHeight = EditorResources::CompileIcon->GetWidth() * 0.9f; + if (ImGui::InvisibleButton("compile", ImVec2(compileIconWidth, compileIconHeight))) + { + m_Model->CompileGraph(); + } + UI::DrawButtonImage(EditorResources::CompileIcon, IM_COL32(235, 165, 36, 200), + IM_COL32(235, 165, 36, 255), + IM_COL32(235, 165, 36, 120)); + + UI::SetTooltip("Compile"); + + if (m_Model->IsPlayerDirty()) + { + UI::ShiftCursorX(-6.0f); + ImGui::Text("*"); + } + } + + // Save Button + { + const float saveIconWidth = static_cast(EditorResources::SaveIcon->GetWidth()); + const float saveIconHeight = static_cast(EditorResources::SaveIcon->GetWidth()); + + if (ImGui::InvisibleButton("saveGraph", ImVec2(saveIconWidth, saveIconHeight))) + { + m_Model->SaveAll(); + } + const int iconOffset = 4; + auto iconRect = UI::GetItemRect(); + iconRect.Min.y += iconOffset; + iconRect.Max.y += iconOffset; + + UI::DrawButtonImage(EditorResources::SaveIcon, IM_COL32(102, 204, 163, 200), + IM_COL32(102, 204, 163, 255), + IM_COL32(102, 204, 163, 120), iconRect); + + UI::SetTooltip("Save graph"); + } + + // Viewport to content + { + if (ImGui::Button("Navigate to Content")) + ed::NavigateToContent(); + } + + const float leftSizeOffset = ImGui::GetCursorPosX(); + + ImGui::Spring(); + + // Playback buttons + // --------------- + + // Play Button + { + if (ImGui::InvisibleButton("PlayGraphButton", ImVec2(ImGui::GetTextLineHeightWithSpacing() + 4.0f, + ImGui::GetTextLineHeightWithSpacing() + 4.0f))) + { + if (m_Model->IsPlayerDirty()) + m_Model->CompileGraph(); + + if (!m_Model->IsPlayerDirty() && m_Model->onPlay) + m_Model->onPlay(); + } + + UI::DrawButtonImage(EditorResources::PlayIcon, IM_COL32(102, 204, 163, 200), + IM_COL32(102, 204, 163, 255), + IM_COL32(102, 204, 163, 120), + UI::RectExpanded(UI::GetItemRect(), 1.0f, 1.0f)); + } + + // Stop Button + { + if (ImGui::InvisibleButton("StopGraphButton", ImVec2(ImGui::GetTextLineHeightWithSpacing() + 4.0f, + ImGui::GetTextLineHeightWithSpacing() + 4.0f))) + { + if (m_Model->onStop) m_Model->onStop(false); + } + + UI::DrawButtonImage(EditorResources::StopIcon, IM_COL32(102, 204, 163, 200), + IM_COL32(102, 204, 163, 255), + IM_COL32(102, 204, 163, 120), + UI::RectExpanded(UI::GetItemRect(), -1.0f, -1.0f)); + } + + ImGui::Spring(1.0f, leftSizeOffset); + + ImGui::Spring(0.0f, spacing); + ImGui::EndHorizontal(); + } + + + void IONodeGraphEditor::DrawDetailsPanel() + { + auto& io = ImGui::GetIO(); + auto boldFont = io.Fonts->Fonts[0]; + + std::vector selectedNodes; + std::vector selectedLinks; + selectedNodes.resize(ed::GetSelectedObjectCount()); + selectedLinks.resize(ed::GetSelectedObjectCount()); + + int nodeCount = ed::GetSelectedNodes(selectedNodes.data(), static_cast(selectedNodes.size())); + int linkCount = ed::GetSelectedLinks(selectedLinks.data(), static_cast(selectedLinks.size())); + + selectedNodes.resize(nodeCount); + selectedLinks.resize(linkCount); + + UI::ShiftCursorY(12.0f); + { + ImGui::CollapsingHeader("##NODES", ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_Leaf); + ImGui::SetItemAllowOverlap(); + ImGui::SameLine(); + UI::ShiftCursorX(-8.0f); + ImGui::Text("NODES"); + } + + if (ImGui::BeginListBox("##nodesListBox", ImVec2(-FLT_MIN, -FLT_MIN))) + { + for (auto& node : m_Model->GetNodes()) + { + UI::ScopedID nodeID((int)node->ID); + auto start = ImGui::GetCursorScreenPos(); + + bool isSelected = std::find(selectedNodes.begin(), selectedNodes.end(), ed::NodeId(node->ID)) != selectedNodes.end(); + if (ImGui::Selectable((node->Name + "##" + std::to_string(node->ID)).c_str(), &isSelected)) + { + if (io.KeyCtrl) + { + if (isSelected) + ed::SelectNode(ed::NodeId(node->ID), true); + else + ed::DeselectNode(ed::NodeId(node->ID)); + } + else + ed::SelectNode(ed::NodeId(node->ID), false); + + ed::NavigateToSelection(); + } + if (UI::IsItemHovered() && !node->State.empty()) + ImGui::SetTooltip("State: %s", node->State.c_str()); + + //? Display node IDs + /*auto id = std::string("(") + std::to_string(node.ID) + ")"; + auto textSize = ImGui::CalcTextSize(id.c_str(), nullptr); + + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - textSize.x); + ImGui::Text(id.c_str());*/ + } + + ImGui::EndListBox(); + } + } + + + void IONodeGraphEditor::DrawNodeDetails(UUID nodeID) + { + auto node = m_Model->FindNode(nodeID); + if (!node) + return; + + auto propertyType = m_Model->GetPropertyTypeOfNode(node); + if (propertyType != EPropertyType::Invalid) + { + std::string_view propertyName; + switch (propertyType) + { + case EPropertyType::Input: propertyName = node->Outputs[0]->Name; break; + case EPropertyType::Output: propertyName = node->Inputs[0]->Name; break; + case EPropertyType::LocalVariable: if(node->Inputs.size() > 0) propertyName = node->Inputs[0]->Name; else propertyName = node->Outputs[0]->Name; break; + default: propertyName = node->Name; break; + } + DrawPropertyDetails(propertyType, propertyName); + return; + } + + //! For now we only draw details for a Comment node + if (node->Type != NodeType::Comment) + return; + + if (UI::PropertyGridHeader("Comment")) + { + UI::BeginPropertyGrid(); + + // Comment colour + { + glm::vec4 colour; + memcpy(&colour.x, &node->Color.Value.x, sizeof(float) * 4); + + if (UI::PropertyColor("Colour", colour)) + memcpy(&node->Color.Value.x, &colour.x, sizeof(float) * 4); + } + + UI::EndPropertyGrid(); + ImGui::TreePop(); + } + } + + + // Note(0x): c++20 would allow this as a lambda inside DrawGraphIO() + template + bool EditAssetField(const std::string& id, choc::value::ValueView& valueView, bool changed) + { + ImGui::PushID((id + "AssetReference").c_str()); + + AssetHandle selected = 0; + Ref asset; + + selected = static_cast(Utils::GetAssetHandleFromValue(valueView)); + + if (AssetManager::IsAssetHandleValid(selected)) + { + asset = AssetManager::GetAsset(selected); + } + // Initialize choc::Value class object with correct asset type using helper function + bool assetDropped = false; + if (UI::AssetReferenceDropTargetButton(id.c_str(), asset, assetType, assetDropped)) + { + ImGui::OpenPopup((id + "AssetPopup").c_str()); + } + + if (assetDropped) + { + Utils::SetAssetHandleValue(valueView, asset->Handle); + changed = true; + } + + bool clear = false; + if (UI::Widgets::AssetSearchPopup((id + "AssetPopup").c_str(), assetType, selected, &clear)) + { + if (clear) + { + selected = 0; + } + Utils::SetAssetHandleValue(valueView, selected); + changed = true; + } + + ImGui::PopID(); + return changed; + } + + + void IONodeGraphEditor::DrawPropertyDetails(EPropertyType graphPropertyType, std::string_view propertyName) + { + EPropertyType propertyType = (EPropertyType)graphPropertyType; + + const float panelWidth = ImGui::GetContentRegionAvail().x; + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Indent(); + + std::string oldName(propertyName); + std::string newName(propertyName); + + choc::value::Value value = m_Model->GetPropertySet(propertyType).GetValue(newName); + + // Name + { + static char buffer[128]{}; + memset(buffer, 0, sizeof(buffer)); + memcpy(buffer, newName.data(), std::min(newName.size(), sizeof(buffer))); + + const auto inputTextFlags = ImGuiInputTextFlags_AutoSelectAll + | ImGuiInputTextFlags_EnterReturnsTrue + | ImGuiInputTextFlags_CallbackAlways; + + ImGui::Text("Input Name"); + ImGui::SameLine(); + UI::ShiftCursorY(-3.0f); + + // TODO: move this into some sort of utility header + auto validateName = [](ImGuiInputTextCallbackData* data) -> int + { + const auto isDigit = [](char c) { return c >= '0' && c <= '9'; }; + const auto isSafeIdentifierChar = [&isDigit](char c) + { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ' ' || isDigit(c); + }; + + const int lastCharPos = glm::max(data->CursorPos - 1, 0); + char lastChar = data->Buf[lastCharPos]; + + if (data->BufTextLen >= 1) + { + // Must not start with '_' or ' ' + if (data->Buf[0] == '_' || data->Buf[0] == ' ' || isDigit(data->Buf[0]) || !isSafeIdentifierChar(data->Buf[0])) + data->DeleteChars(0, 1); + } + + if (data->BufTextLen < 3) + return 0; + + if (lastChar == ' ') + { + // Block double whitespaces + std::string_view str(data->Buf); + size_t doubleSpacePos = str.find(" "); + if (doubleSpacePos != std::string::npos) + data->DeleteChars((int)doubleSpacePos, 1); + + return 1; + } + else if (!isSafeIdentifierChar(lastChar)) + { + data->DeleteChars(lastCharPos, 1); + } + + return 0; + }; + + UI::InputText("##inputName", buffer, 127, inputTextFlags, validateName); + + if (ImGui::IsItemDeactivatedAfterEdit() && buffer[0]) + { + if (m_Model->IsGraphPropertyNameValid(propertyType, buffer)) + { + // Rename item in the list + newName = buffer; + newName = choc::text::trim(newName); + m_Model->RenameProperty(propertyType, oldName, newName); + + m_SelectedItem->Name = buffer; + } + else + { + // TODO: show some informative warning + } + } + } + + // Type & Value + { + const auto& types = m_Model->GetInputTypes(); + + choc::value::Type type = value.getType(); + choc::value::Type arrayType; + bool isArrayOrVector = false; + if (value.isArray()) + { + arrayType = type; + isArrayOrVector = true; + type = type.getElementType(); + } + + // find index of type in types + int32_t selected = -1; + auto typeName = Utils::GetTypeName(value); + for (int i = 0; i < types.size(); i++) + { + if (types[i] == typeName) + { + selected = i; + break; + } + } + if (selected == -1) + { + SE_CORE_ERROR("Invalid type of Graph Input!"); + selected = 0; + } + + auto createValueFromSelectedIndex = [&types](int32_t index) + { + const auto& type = Utils::GetTypeFromName(types[index]); + auto value = choc::value::Value(type); + return value; + }; + + // Change the type the value, or the type of array element + ImGui::SetCursorPosX(0.0f); + + const float checkboxWidth = 84.0f; + ImGui::BeginChildFrame(ImGui::GetID("TypeRegion"), ImVec2(ImGui::GetContentRegionAvail().x - checkboxWidth, ImGui::GetFrameHeight() * 1.4f), ImGuiWindowFlags_NoBackground); + ImGui::Columns(2); + + auto getColourForType = [&](int optionNumber) + { + return GetModel()->GetValueColor(createValueFromSelectedIndex(optionNumber)); + }; + + if (UI::PropertyDropdown("Type", types, (int32_t)types.size(), &selected, getColourForType)) + { + choc::value::Value newValue = createValueFromSelectedIndex(selected); + + if (isArrayOrVector) + { + choc::value::Value arrayValue = choc::value::createArray(value.size(), [&](uint32_t i) { return choc::value::Value(newValue.getType()); }); + + m_Model->ChangePropertyType(propertyType, newName, arrayValue); + } + else + { + m_Model->ChangePropertyType(propertyType, newName, newValue); + } + } + ImGui::EndColumns(); + ImGui::EndChildFrame(); + + // Update value handle in case type or name has changed + value = m_Model->GetPropertySet(propertyType).GetValue(newName); + isArrayOrVector = value.isArray(); + + // Switch between Array vs Value + + ImGui::SameLine(0.0f, 0.0f); + UI::ShiftCursorY(3); + + if (value.isVoid()) + ImGui::BeginDisabled(); + + bool isArray = value.isArray(); + if (UI::Checkbox("##IsArray", &isArray)) + { + if (isArray) // Became array + { + choc::value::Value arrayValue = choc::value::createEmptyArray(); + arrayValue.addArrayElement(value); + + m_Model->ChangePropertyType(propertyType, newName, arrayValue); + isArrayOrVector = true; + value = arrayValue; + } + else // Became simple + { + auto newValue = choc::value::Value(value.getType().getElementType()); + + m_Model->ChangePropertyType(propertyType, newName, newValue); + isArrayOrVector = false; + value = newValue; + } + } + ImGui::SameLine(); + ImGui::TextUnformatted("Is Array"); + + if (value.isVoid()) + ImGui::EndDisabled(); + + ImGui::Unindent(); + + ImGui::Dummy(ImVec2(panelWidth, 10.0f)); + + // Assign default value to the Value or Array elements + + auto valueEditField = [](const char* label, choc::value::ValueView& valueView, bool* removeButton = nullptr) + { + ImGui::Text(label); + ImGui::NextColumn(); + ImGui::PushItemWidth(-ImGui::GetFrameHeight()); + + bool changed = false; + const std::string id = "##" + std::string(label); + + if (valueView.isFloat()) + { + float v = valueView.get(); + if (UI::DragFloat(id.c_str(), &v, 0.01f, 0.0f, 0.0f, "%.2f")) + { + valueView.set(v); + changed = true; + } + + } + else if (valueView.isInt32()) + { + int32_t v = valueView.get(); + if (UI::DragInt32(id.c_str(), &v, 0.1f, 0, 0)) + { + valueView.set(v); + changed = true; + } + } + else if (valueView.isBool()) + { + bool v = valueView.get(); + if (UI::Checkbox(id.c_str(), &v)) + { + valueView.set(v); + changed = true; + } + } + else if (valueView.isString()) + { + std::string v = valueView.get(); + + const auto inputTextFlags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue; + UI::InputText(id.c_str(), &v, inputTextFlags); + if (ImGui::IsItemDeactivatedAfterEdit()) + { + valueView.set(v); + changed = true; + } + } + else if (Utils::IsAssetHandle(valueView)) changed = EditAssetField(id, valueView, changed); + //else if (Utils::IsAssetHandle(valueView)) changed = EditAssetField(id, valueView, changed); + //else if (Utils::IsAssetHandle(valueView)) changed = EditAssetField(id, valueView, changed); + else if (valueView.isObjectWithClassName(type::type_name())) + { + auto v = static_cast(valueView.getRawData()); + bool manuallyEdited = false; + changed = UI::Widgets::EditVec3(id.c_str(), ImVec2(ImGui::GetContentRegionAvail().x - ImGui::GetFrameHeight() - 8.0f, ImGui::GetFrameHeightWithSpacing()), 0.0f, manuallyEdited, *v); + } + else + { + SE_CORE_ERROR("Invalid type of Graph Input!"); + } + + ImGui::PopItemWidth(); + + if (removeButton != nullptr) + { + ImGui::SameLine(); + + ImGui::PushID((id + "removeButton").c_str()); + + if (ImGui::SmallButton("x")) + *removeButton = true; + + ImGui::PopID(); + } + + ImGui::NextColumn(); + + return changed; + }; + + + ImGui::GetWindowDrawList()->AddRectFilled( + ImGui::GetCursorScreenPos() - ImVec2(0.0f, 2.0f), + ImGui::GetCursorScreenPos() + ImVec2(panelWidth, ImGui::GetTextLineHeightWithSpacing()), + ImColor(ImGui::GetStyle().Colors[ImGuiCol_HeaderActive]), 2.5f); + + ImGui::Spacing(); ImGui::SameLine(); UI::ShiftCursorY(2.0f); + ImGui::Text("Default Value"); + + ImGui::Spacing(); + ImGui::Dummy(ImVec2(panelWidth, 4.0f)); + + ImGui::Indent(); + + const bool isTriggerType = value.isVoid(); + + if (!isTriggerType && !isArrayOrVector) + { + ImGui::Columns(2); + if (valueEditField("Value", value.getViewReference())) + { + m_Model->SetPropertyValue(propertyType, newName, value); + } + ImGui::EndColumns(); + } + else if (!isTriggerType) // Array + { + ImGui::BeginHorizontal("ArrayElementsInfo", { ImGui::GetContentRegionAvail().x, 0.0f }); + uint32_t size = value.size(); + ImGui::Text((std::to_string(size) + " Array elements").c_str()); + + ImGui::Spring(); + + //? For now limiting max number of elements in array to fit into + //? choc's small vector optimization and to prevent large amount + //? of data being copied to SoundGraph player + const bool reachedMaxArraySize = value.size() >= 32; + if (reachedMaxArraySize) + { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + } + + bool changed = false; + bool invalidatePlayer = false; + + if (ImGui::SmallButton("Add Element")) + { + value.addArrayElement(choc::value::Value(value.getType().getElementType())); + + changed = true; + invalidatePlayer = true; + } + + if (reachedMaxArraySize) + { + ImGui::PopItemFlag(); // ImGuiItemFlags_Disabled + ImGui::PopStyleColor(); // ImGuiCol_Text + } + + ImGui::Spring(0.0f, 4.0f); + ImGui::EndHorizontal(); + + ImGui::Spacing(); + + ImGui::Columns(2); + + int indexToRemove = -1; + UI::PushID(); + for (uint32_t i = 0; i < value.size(); ++i) + { + choc::value::ValueView vv = value[i]; + + bool removeThis = false; + if (valueEditField(("[ " + std::to_string(i) + " ]").c_str(), vv, &removeThis)) + changed = true; + + if (value.size() > 1 && removeThis) indexToRemove = i; + } + UI::PopID(); + + if (indexToRemove >= 0) + { + bool skipIteration = false; + value = choc::value::createArray(value.size() - 1, [&value, &skipIteration, indexToRemove](uint32_t i) + { + if (!skipIteration) + skipIteration = i == indexToRemove; + return skipIteration ? value[i + 1] : value[i]; + }); + + changed = true; + invalidatePlayer = true; + } + + if (invalidatePlayer) + m_Model->InvalidatePlayer(); + + if (changed) + { + m_Model->SetPropertyValue(propertyType, newName, value); + } + + ImGui::EndColumns(); + } + // TODO: invalidate connections that were using this input + + ImGui::Unindent(); + } + } + + + void IONodeGraphEditor::DrawGraphIO() + { + const float panelWidth = ImGui::GetContentRegionAvail().x; + + auto addInputButton = [&]() + { + ImGui::SameLine(panelWidth - ImGui::GetFrameHeight() - ImGui::CalcTextSize("Add Input").x); + if (ImGui::SmallButton("Add Input")) + { + std::string name = m_Model->AddPropertyToGraph(EPropertyType::Input, choc::value::createFloat32(0.0f)); + if(!name.empty()) + m_SelectedItem->Select(EPropertyType::Input, name); + } + }; + + const int leafFlags = ImGuiTreeNodeFlags_Leaf + | ImGuiTreeNodeFlags_NoTreePushOnOpen + | ImGuiTreeNodeFlags_SpanAvailWidth + | ImGuiTreeNodeFlags_FramePadding + | ImGuiTreeNodeFlags_AllowItemOverlap; + + const int headerFlags = ImGuiTreeNodeFlags_CollapsingHeader + | ImGuiTreeNodeFlags_AllowItemOverlap + | ImGuiTreeNodeFlags_DefaultOpen; + + // INPUTS list + ImGui::Spacing(); + if (UI::PropertyGridHeader("INPUTS")) + { + addInputButton(); + + // For now, don't show "Input Action" node. + // There are no "details" for these. + //ImGui::TreeNodeEx("Input Action", leafFlags); + + std::string inputToRemove; + + for (auto& input : m_Model->GetInputs().GetNames()) + { + bool selected = m_SelectedItem->IsSelected(EPropertyType::Input, input); + int flags = selected ? leafFlags | ImGuiTreeNodeFlags_Selected : leafFlags; + + // Colouring text, just because + ImColor textColour = m_Model->GetValueColor(m_Model->GetInputs().GetValue(input)); + + ImGui::PushStyleColor(ImGuiCol_Text, textColour.Value); + ImGui::TreeNodeEx(input.c_str(), flags); + ImGui::PopStyleColor(); + + if (ImGui::BeginDragDropSource()) + { + ImGui::TextUnformatted(input.c_str()); + ImGui::SetDragDropPayload("input_payload", input.c_str(), input.size() + 1); + ImGui::EndDragDropSource(); + } + + if (ImGui::IsItemClicked()) + m_SelectedItem->Select(EPropertyType::Input, input); + + // Remove button + ImGui::SameLine(); + ImGui::SetCursorPosX(panelWidth - ImGui::GetFrameHeight()); + + ImGui::PushID((input + "removeInput").c_str()); + if (ImGui::SmallButton("x")) + inputToRemove = input; + ImGui::PopID(); + } + + if (!inputToRemove.empty()) + { + if (inputToRemove == m_SelectedItem->Name) + m_SelectedItem->Clear(); + + m_Model->RemovePropertyFromGraph(EPropertyType::Input, inputToRemove); + } + + ImGui::TreePop(); + } + else + { + addInputButton(); + } + + // For now, do not show Outputs list, because: + // a) There are no details to display for selected output + // b) More than one output is not supported + // c) Adding output is not supported + //auto addOutputButton = [&] + //{ + // ImGui::SameLine(panelWidth - ImGui::GetFrameHeight() - ImGui::CalcTextSize("Add Output").x); + // if (ImGui::SmallButton("Add Output")) + // { + // // TODO: add output + // HZ_CORE_WARN("Add Output to Graph not implemented."); + // } + //}; + // + //// OUTPUTS list + //if (UI::PropertyGridHeader("OUTPUTS")) + //{ + // addOutputButton(); + // + // Note: graph isnt necessarily Audio, so "Output Audio" is not necessarily + // what the output is + // ImGui::TreeNodeEx("Output Audio", leafFlags); + // + // ImGui::TreePop(); + //} + //else + //{ + // addOutputButton(); + //} + + auto addVariableButton = [&]() + { + ImGui::SameLine(panelWidth - ImGui::GetFrameHeight() - ImGui::CalcTextSize("Add Variable").x); + if (ImGui::SmallButton("Add Variable")) + { + std::string name = m_Model->AddPropertyToGraph(EPropertyType::LocalVariable, choc::value::createFloat32(0.0f)); + if (!name.empty()) + m_SelectedItem->Select(EPropertyType::LocalVariable, name); + } + }; + + // Variables / Reroutes + if (UI::PropertyGridHeader("LOCAL VARIABLES")) + { + addVariableButton(); + + std::string variableToRemove; + for (auto& input : m_Model->GetLocalVariables().GetNames()) + { + bool selected = m_SelectedItem->IsSelected(EPropertyType::LocalVariable, input); + int flags = selected ? leafFlags | ImGuiTreeNodeFlags_Selected : leafFlags; + + // Colouring text, just because + ImColor textColor = m_Model->GetValueColor(m_Model->GetLocalVariables().GetValue(input)); + + ImGui::PushStyleColor(ImGuiCol_Text, textColor.Value); + ImGui::TreeNodeEx(input.c_str(), flags); + ImGui::PopStyleColor(); + + if (ImGui::BeginDragDropSource()) + { + ImGui::TextUnformatted(input.c_str()); + ImGui::SameLine(); + ImGui::TextUnformatted((ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) ? "(set)" : "(get)"); + ImGui::SetDragDropPayload("localvar_payload", input.c_str(), input.size() + 1); + ImGui::EndDragDropSource(); + } + + if (ImGui::IsItemClicked()) + m_SelectedItem->Select(EPropertyType::LocalVariable, input); + + // Remove button + ImGui::SameLine(); + ImGui::SetCursorPosX(panelWidth - ImGui::GetFrameHeight()); + + ImGui::PushID((input + "removeVariable").c_str()); + if (ImGui::SmallButton("x")) + variableToRemove = input; + ImGui::PopID(); + } + + if (!variableToRemove.empty()) + { + if (variableToRemove == m_SelectedItem->Name) + m_SelectedItem->Clear(); + + m_Model->RemovePropertyFromGraph(EPropertyType::LocalVariable, variableToRemove); + } + ImGui::TreePop(); + } + else + { + addVariableButton(); + } + } + + + void IONodeGraphEditor::DrawSelectedDetails() + { + const float panelWidth = ImGui::GetContentRegionAvail().x; + + if (*m_SelectedItem) + { + if (m_SelectedItem->SelectionType == ESelectionType::Node) + DrawNodeDetails(m_SelectedItem->ID); + else + DrawPropertyDetails(m_SelectedItem->GetPropertyType(), m_SelectedItem->Name); + } + + //? DBG Info + ImGui::Dummy(ImVec2(panelWidth, 40.0f)); + UI::Checkbox("##IDs", &m_ShowNodeIDs); + ImGui::SameLine(); + ImGui::TextUnformatted("Show IDs"); + ImGui::SameLine(0, 20.0f); + UI::Checkbox("##Indices", &m_ShowSortIndices); + ImGui::SameLine(); + ImGui::TextUnformatted("Show Sort Indices"); + } + +} // namespace Hazel diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditor.h b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditor.h new file mode 100644 index 00000000..ace17067 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditor.h @@ -0,0 +1,279 @@ +#pragma once + +#include "Nodes.h" +#include "NodeEditorModel.h" +#include "NodeGraphUtils.h" + +#include "StarEngine/Editor/AssetEditorPanel.h" + +namespace ax::NodeEditor { + + struct Style; + struct EditorContext; + + namespace Utilities { + struct BlueprintNodeBuilder; // TODO: rename it and rewrite it to use Hazel::Texture2D + } +} + +namespace StarEngine { + + using NodeBuilder = ax::NodeEditor::Utilities::BlueprintNodeBuilder; + + struct SelectAssetPinContext + { + UUID PinID = 0; + AssetHandle SelectedAssetHandle = 0; + AssetType AssetType = AssetType::None; + }; + + class NodeEditorDraw + { + public: + static float GetBestWidthForEnumCombo(const std::vector& enumTokens); + static bool PropertyBool(choc::value::Value& value); + static bool PropertyFloat(choc::value::Value& value, const char* format = "%.2f"); + static bool PropertyInt(choc::value::Value& value, const char* format = "%d"); + static bool PropertyVec3(std::string_view label, choc::value::Value& value, const char* format = "%.2f"); + static bool PropertyString(choc::value::Value& value); + static bool PropertyEnum(int enumValue, Pin* pin, bool& openEnumPopup, + std::function constructValue = [](int selected) {return choc::value::createInt32(selected);}); + + template + static bool PropertyAsset(choc::value::Value& value, Pin* inputPin, bool& openAssetPopup) + { + SE_CORE_VERIFY(inputPin); + + bool modified = false; + + AssetHandle selected = 0; + Ref asset; + + // TODO: position popup below the property button + //auto canvaslPos = ImGui::GetCursorScreenPos(); + //auto screenPos = ed::ScreenToCanvas(canvaslPos); + + bool assetDropped = false; + ImGui::SetNextItemWidth(90.0f); + + if (Utils::IsAssetHandle(value)) + { + selected = Utils::GetAssetHandleFromValue(value); + } + + if (AssetManager::IsAssetHandleValid(selected)) + { + asset = AssetManager::GetAsset(selected); + } + // Initialize choc::Value class object with correct asset type using helper function + if (UI::AssetReferenceDropTargetButton("Asset", asset, assetType, assetDropped)) + { + //HZ_CORE_WARN("Asset clicked"); + openAssetPopup = true; + + s_SelectAssetContext = { inputPin->ID, selected, assetType }; + } + + if (assetDropped) + { + value = Utils::CreateAssetHandleValue(asset->Handle); + modified = true; + } + + return modified; + } + + public: + inline static SelectAssetPinContext s_SelectAssetContext; + + }; + + + class NodeGraphEditorBase : public AssetEditor + { + public: + NodeGraphEditorBase(const char* id); + virtual ~NodeGraphEditorBase(); + + // If you override this, make sure to call base class method from subclass. + void SetAsset(const Ref& asset) override; + + std::vector GetSelectedNodeIDs() const; + + protected: + virtual bool InitializeEditor(); + + void Render() override final; + + // Called after drawing main canvas. + // Use this to draw extra windows. + virtual void OnRender() {}; + + // Called before ending draw of main canvas. + // Use this to draw more stuff on the canvas. + virtual void OnRenderOnCanvas(ImVec2 min, ImVec2 max) {}; + + bool DeactivateInputIfDragging(const bool wasActive); + + virtual NodeEditorModel* GetModel() = 0; + virtual const NodeEditorModel* GetModel() const = 0; + virtual const char* GetIconForSimpleNode(const std::string& simpleNodeIdentifier) const { return nullptr; } + + void OnOpen() override {}; + void OnClose() override; // If you override this, make sure to call base class method from subclass. + + void LoadSettings(); + + virtual void InitializeEditorStyle(ax::NodeEditor::Style& style); + virtual int GetPinIconSize() { return 24; } + + struct PinPropertyContext + { + Pin* pin = nullptr; + NodeEditorModel* model = nullptr; + bool OpenAssetPopup = false; + bool OpenEnumPopup = false; + }; + virtual bool DrawPinPropertyEdit(PinPropertyContext& context); + + bool IsPinAcceptingLink(const Pin* pin) { return m_AcceptingLinkPins.first == pin || m_AcceptingLinkPins.second == pin; } + ImColor GetIconColor(int pinTypeID) const; + + ImGuiWindowClass* GetWindowClass() { return &m_WindowClass; } + void EnsureWindowIsDocked(ImGuiWindow* childWindow); + + std::pair DrawGraph(const char* id); + + virtual void DrawNodes(PinPropertyContext& pinContext); + virtual void DrawLinks(); + + virtual void DrawNode(Node* node, NodeBuilder& builder, PinPropertyContext& pinContext); + virtual void DrawCommentNode(Node* node); + + virtual void DrawNodeHeader(Node* node, NodeBuilder& builder); + virtual void DrawNodeMiddle(Node* node, NodeBuilder& builder); + virtual void DrawNodeInputs(Node* node, NodeBuilder& builder, PinPropertyContext& pinContext); + virtual void DrawNodeOutputs(Node* node, NodeBuilder& builder); + virtual void DrawPinIcon(const Pin* pin, bool connected, int alpha); + + virtual void CheckContextMenus(); + virtual void DrawNodeContextMenu(Node* node); + virtual void DrawPinContextMenu(Pin* pin); + virtual void DrawLinkContextMenu(Link* link); + virtual void DrawBackgroundContextMenu(bool& isNewNodePoppedUp, ImVec2& newNodePostion); + + virtual void DrawDeferredComboBoxes(PinPropertyContext& pinContext); + + protected: + Ref m_NodeBuilderTexture; + ax::NodeEditor::EditorContext* m_Editor = nullptr; + bool m_Initialized = false; + bool m_FirstFrame = true; + + // If subclass graph editor supports drag-drop onto canvas + // then the drop targets can be checked and accepted as appropriate in this callback + std::function onDragDropOntoCanvas = nullptr; + + // If subclass graph editor has any custom nodes, + // the popup UI items for them can be added on this callback + std::function onNodeListPopup = nullptr; + + // If subclass graph editor wants to do custom action when new node is created + // from node popup, it can be done in this callback. + std::function onNewNodeFromPopup = nullptr; + + // If subclass graph editor wants to do custom action if new node popup is cancelled + // it can be done in this callback. + std::function onNewNodePopupCancelled = nullptr; + + // A pair of nodes that are accepting a link connection in this frame + std::pair m_AcceptingLinkPins{ nullptr, nullptr }; + + // Render local states + struct ContextState; + ContextState* m_State = nullptr; + + // For debugging + bool m_ShowNodeIDs = false; + bool m_ShowSortIndices = false; + + private: + std::string m_CanvasName; + ImGuiWindowClass m_WindowClass; + ImGuiID m_MainDockID = 0; + std::unordered_map m_DockIDs; + + std::string m_GraphStatePath; + }; + + + class IONodeGraphEditor : public NodeGraphEditorBase + { + public: + IONodeGraphEditor(const char* id); + virtual ~IONodeGraphEditor(); + + protected: + NodeEditorModel* GetModel() override { return m_Model.get(); } + const NodeEditorModel* GetModel() const override { return m_Model.get(); } + virtual void SetModel(Scope model) { m_Model = std::move(model); ClearSelection(); } + + void OnRender() override; + + ImGuiWindowFlags GetWindowFlags() override; + void OnWindowStylePush() override; + void OnWindowStylePop() override; + + const char* GetIconForSimpleNode(const std::string& simpleNodeIdentifier) const override; + + void SelectNode(UUID id); + bool IsAnyNodeSelected() const; + void ClearSelection(); + + void DrawToolbar(); + void DrawDetailsPanel(); + void DrawNodeDetails(UUID nodeID); + void DrawPropertyDetails(EPropertyType propertyType, std::string_view propertyName); + virtual void DrawGraphIO(); + virtual void DrawSelectedDetails(); + + private: + enum class ESelectionType + { + Invalid = 0, + Input, + Output, + LocalVariable, + Node + }; + + struct SelectedItem + { + ESelectionType SelectionType{ ESelectionType::Invalid }; + UUID ID = 0; + + std::string Name; + + operator bool() const; + bool IsSelected(EPropertyType type, std::string_view name) const; + + /** Select property type */ + void Select(EPropertyType type, std::string_view name); + + void SelectNode(UUID id); + void Clear(); + EPropertyType GetPropertyType() const; + ESelectionType FromPropertyType(EPropertyType type) const; + }; + + Scope m_Model = nullptr; + Scope m_SelectedItem = nullptr; + + std::string m_ToolbarName; + std::string m_GraphInputsName; + std::string m_GraphDetailsName; + std::string m_DetailsName; + }; + +} // namespace StarEngine + diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditorContext.h b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditorContext.h new file mode 100644 index 00000000..9aef3c9a --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphEditorContext.h @@ -0,0 +1,36 @@ +#pragma once + +// This file exists to avoid having to include imgui_node_editor.h in NodeGraphEditor.h + +#include "NodeGraphEditor.h" +#include "imgui-node-editor/imgui_node_editor.h" + +namespace StarEngine { + + struct NodeGraphEditorBase::ContextState + { + bool CreateNewNode = false; + bool NewNodePopupOpening = false; + UUID NewNodeLinkPinId = 0; + Pin* NewLinkPin = nullptr; + bool DraggingInputField = false; + + UUID EditNodeId = 0; + + ax::NodeEditor::NodeId ContextNodeId = 0; + ax::NodeEditor::LinkId ContextLinkId = 0; + ax::NodeEditor::PinId ContextPinId = 0; + + // For state machine graphs + UUID TransitionStartNode = 0; + UUID HoveredNode = 0; + UUID HoveredTransition = 0; + ImVec2 LastTransitionEndPoint = { 0.0f, 0.0f }; + + // For blend spaces + UUID HoveredVertex = 0; + UUID DraggingVertex = 0; + UUID DraggedVertex = 0; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphUtils.cpp b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphUtils.cpp new file mode 100644 index 00000000..bb734a0d --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphUtils.cpp @@ -0,0 +1,44 @@ +#include "sepch.h" +#include "NodeGraphUtils.h" +#include "Nodes.h" +#include "StarEngine/Reflection/TypeName.h" + +namespace StarEngine::Utils { + + std::string GetTypeName(const choc::value::Value& v) + { + bool isArray = v.isArray(); + choc::value::Type type = isArray ? v.getType().getElementType() : v.getType(); + + if (type.isFloat()) return "Float"; + else if (type.isInt32()) return "Int"; + else if (type.isBool()) return "Bool"; + else if (type.isString()) return "String"; + else if (type.isVoid()) return "Trigger"; + else if (IsAssetHandle(type)) return "AudioAsset"; + else if (IsAssetHandle(type)) return "AnimationAsset"; + else if (IsAssetHandle(type)) return "SkeletonAsset"; + else if (type.isObjectWithClassName(type::type_name())) return "Vec3"; + else SE_CORE_ERROR("Custom object type encountered. This needs to be handled!"); + + return "invalid"; + } + + + choc::value::Type GetTypeFromName(const std::string_view name) + { + if (name == "Float") return choc::value::Type::createFloat32(); + else if (name == "Int") return choc::value::Type::createInt32(); + else if (name == "Bool") return choc::value::Type::createBool(); + else if (name == "String") return choc::value::Type::createString(); + else if (name == "Trigger") return choc::value::Type(); + else if (name == "AudioAsset") return CreateAssetHandleType(); + else if (name == "AnimationAsset") return CreateAssetHandleType(); + else if (name == "SkeletonAsset") return CreateAssetHandleType(); + else if (name == "Vec3") return ValueFrom(glm::vec3{}).getType(); + else SE_CORE_ERROR("Unsupported type"); + return choc::value::Type::createInt32(); + + } + +} diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphUtils.h b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphUtils.h new file mode 100644 index 00000000..e4c75237 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/NodeGraphUtils.h @@ -0,0 +1,163 @@ +#pragma once + +#include "StarEngine/Asset/Asset.h" +#include "StarEngine/Asset/AssetTypes.h" + +#include +#include + +#include + +#include +#include + +namespace StarEngine::Utils { + + std::string GetTypeName(const choc::value::Value& v); + choc::value::Type GetTypeFromName(const std::string_view name); + + + template + bool IsAssetHandle(const T& t) + { + static_assert(assetType == AssetType::Audio || assetType == AssetType::Animation || assetType == AssetType::Skeleton, "IsAssetHandle requires AssetType::Audio, AssetType::Animation, or AssetType::Skeleton!"); + static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "IsAssetHandle requires T to be choc::value::Type, choc::value::Value, or choc::value::ValueView"); + + if constexpr (assetType == AssetType::Audio) return t.isObjectWithClassName("AudioAsset") || t.isObjectWithClassName("UUID"); + if constexpr (assetType == AssetType::Animation) return t.isObjectWithClassName("AnimationAsset"); + if constexpr (assetType == AssetType::Skeleton) return t.isObjectWithClassName("SkeletonAsset"); + + return false; + } + + template + bool IsAssetHandle(const T& t) + { + return IsAssetHandle(t) || IsAssetHandle(t) || IsAssetHandle(t); + } + + + template + choc::value::Type CreateAssetHandleType() + { + static_assert(assetType == AssetType::Audio || assetType == AssetType::Animation || assetType == AssetType::Skeleton, "CreateAssetHandleType requires AssetType::Audio, AssetType::Animation, or AssetType::Skeleton!"); + + choc::value::Type t; + if constexpr (assetType == AssetType::Audio) t = choc::value::Type::createObject("AudioAsset"); + if constexpr (assetType == AssetType::Animation) t = choc::value::Type::createObject("AnimationAsset"); + if constexpr (assetType == AssetType::Skeleton) t = choc::value::Type::createObject("SkeletonAsset"); + + t.addObjectMember("Value", choc::value::Type::createInt64()); + return t; + } + + + template + choc::value::Value CreateAssetHandleValue(AssetHandle id) + { + static_assert(assetType == AssetType::Audio || assetType == AssetType::Animation || assetType == AssetType::Skeleton, "CreateAssetHandleValue requires AssetType::Audio, AssetType::Animation, or AssetType::Skeleton!"); + + if constexpr (assetType == AssetType::Audio) return choc::value::createObject("AudioAsset", "Value", choc::value::Value((int64_t)id)); + if constexpr (assetType == AssetType::Animation) return choc::value::createObject("AnimationAsset", "Value", choc::value::Value((int64_t)id)); + if constexpr (assetType == AssetType::Skeleton) return choc::value::createObject("SkeletonAsset", "Value", choc::value::Value((int64_t)id)); + } + + + inline choc::value::Value CreateAssetHandleValue(AssetType assetType, AssetHandle id) + { + switch (assetType) + { + case AssetType::Audio: return CreateAssetHandleValue(id); + case AssetType::Animation: return CreateAssetHandleValue(id); + case AssetType::Skeleton: return CreateAssetHandleValue(id); + default: + SE_CORE_ASSERT(false, "Unknown asset type!"); + return {}; + } + } + + + template + AssetHandle GetAssetHandleFromValue(const T& v) + { + static_assert(std::is_same_v || std::is_same_v, "GetAssetHandleFromValue requires T to be choc::value::Value, or choc::value::ValueView"); + SE_CORE_ASSERT(IsAssetHandle(v)); + + // Assuming asset handle value stored as int64 [Data]["Value"] member in choc Value object + return (AssetHandle)(v["Value"].getInt64()); + } + + + template + inline void SetAssetHandleValue(T& v, AssetHandle value) + { + static_assert(std::is_same_v || std::is_same_v, "SetAssetHandleValue requires T to be choc::value::Value, or choc::value::ValueView"); + SE_CORE_ASSERT(IsAssetHandle(v)); + + v["Value"].set((int64_t)value); + } + + inline bool isDigit(char c) { return c >= '0' && c <= '9'; }; + inline bool isSafeIdentifierChar(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || isDigit(c); }; + inline bool containsChar(const std::string& s, char c) noexcept + { + return s.find(c) != std::string::npos; + }; + inline auto containsChar(const char* s, char c) noexcept + { + if (s == nullptr) + return false; + + for (; *s != 0; ++s) + if (*s == c) + return true; + + return false; + }; + + inline std::string MakeSafeIdentifier(std::string name) + { + // TODO: this is taken straight from soul stuff, move this to some sort of utility header + auto makeSafeIdentifierName = [](std::string s) -> std::string + { + for (auto& c : s) + if (containsChar(" ,./;", c)) + c = '_'; + + s.erase(std::remove_if(s.begin(), s.end(), [&](char c) { return !isSafeIdentifierChar(c); }), s.end()); + + // Identifiers can't start with a digit + if (isDigit(s[0])) + s = "_" + s; + + return s; + }; + name = makeSafeIdentifierName(name); + + constexpr const char* reservedWords[] = + { + "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", + "bitand", "bitor", "bool", "break", "case", "catch", "char", "char8_t", "char16_t", "char32_t", "class", + "compl", "concept", "const", "consteval", "constexpr", "constinit", "const_cast", "continue", "co_await", + "co_return", "co_yield", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", + "explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "inline", "int", "long", + "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", + "protected", "public", "reflexpr", "register", "reinterpret_cast", "requires", "return", "short", "signed", + "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template", "this", + "thread_local", "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", + "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq" + }; + + for (auto r : reservedWords) + if (name == r) + return name + "_"; + + return name; + } + + inline std::string MangleStructOrFunctionName(const std::string& namespacedName) + { + return MakeSafeIdentifier(choc::text::replace(namespacedName, ":", "_")); + } + +} diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/Nodes.cpp b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/Nodes.cpp new file mode 100644 index 00000000..d616bb62 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/Nodes.cpp @@ -0,0 +1,100 @@ +#include +#include "Nodes.h" + +#include + +ImVec2 StarEngine::Node::GetPosition() const +{ + ax::NodeEditor::Detail::NodeSettings settings(0); + ax::NodeEditor::Detail::NodeSettings::Parse(State, settings); + return settings.m_Location; +} + + +void StarEngine::Node::SetPosition(ImVec2 pos) +{ + ax::NodeEditor::Detail::NodeSettings settings(0); + ax::NodeEditor::Detail::NodeSettings::Parse(State, settings); + settings.m_Location = pos; + State = settings.Serialize().dump(); +} + +// These are just examples and are not actually needed to build Sound or Animation node graphs +#if 0 +#include "choc/text/choc_StringUtilities.h" + +namespace StarEngine::Nodes { + + struct ExamplePin : Pin + { + using Pin::Pin; + + ExamplePin(UUID ID, std::string_view name, EPinType type) + : Pin(ID, name), Type(type) + { + } + + int GetType() const override { return (int)Type; } + std::string_view GetTypeString() const override { return magic_enum::enum_name(Type); } + + EPinType Type; + }; + + struct ExampleNode : Node + { + using Node::Node; + + virtual int GetTypeID() const override { return 0; } + }; + + //========================================================================== + /// Utility Nodes + class Utility + { + public: + static Node* Comment() + { + const std::string nodeName = choc::text::replace(__func__, "_", " "); + + auto* node = new ExampleNode(0, nodeName.c_str()); + node->Category = "Utility"; + + node->Type = NodeType::Comment; + node->Size = ImVec2(300, 200); + + return node; + } + + static Node* Message() + { + const std::string nodeName = choc::text::replace(__func__, "_", " "); + + auto* node = new ExampleNode(0, nodeName.c_str(), ImColor(128, 195, 248)); + node->Category = "Utility"; + + node->Type = NodeType::Simple; + node->Outputs.push_back(new ExamplePin(0, "Message", EPinType::String)); + + return node; + } + + static Node* Dummy_Node() + { + const std::string nodeName = choc::text::replace(__func__, "_", " "); + + auto* node = new ExampleNode(0, nodeName.c_str(), ImColor(128, 195, 248)); + node->Category = "Utility"; + + node->Type = NodeType::Blueprint; + + node->Inputs.push_back(new ExamplePin(0, "Message", EPinType::String)); + + node->Outputs.push_back(new ExamplePin(0, "Message", EPinType::String)); + + return node; + } + }; + +} // namespace StarEngine::Nodes +#endif + diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/Nodes.h b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/Nodes.h new file mode 100644 index 00000000..54836042 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/Nodes.h @@ -0,0 +1,543 @@ +#pragma once + +#include "PropertySet.h" + +#include "StarEngine/Core/Assert.h" +#include "StarEngine/Core/Log.h" +#include "StarEngine/Core/UUID.h" +#include "StarEngine/Reflection/TypeName.h" +#include "StarEngine/Utilities/StringUtils.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + + +namespace StarEngine { + + //! Add your custom pin / value types + enum EPinType : int + { + Flow, + Bool, + Int, + Float, + String, + Object, + Function, + DelegatePin, + Enum + }; + + enum class PinKind + { + Output, + Input + }; + + enum class StorageKind + { + Value = 0, + Array + }; + + //! Add your custom Node types + enum class NodeType + { + Blueprint, + Simple, + Comment, + Subroutine + }; + + enum class EPropertyType : uint8_t + { + Invalid = 0, + Input, + Output, + LocalVariable + }; + + //! Set color for your pin / value types + inline ImColor GetIconColor(EPinType type) + { + switch (type) + { + default: + case EPinType::Flow: return ImColor(200, 200, 200); + case EPinType::Bool: return ImColor(220, 48, 48); + case EPinType::Int: return ImColor(68, 201, 156); + case EPinType::Float: return ImColor(147, 226, 74); + case EPinType::String: return ImColor(194, 75, 227); + case EPinType::Object: return ImColor(51, 150, 215); + case EPinType::Function: return ImColor(200, 200, 200); + case EPinType::DelegatePin: return ImColor(255, 48, 48); + case EPinType::Enum: return ImColor(2, 92, 48); + } + }; + + + //================================================================= + struct Node; + + struct Token + { + const std::string_view name; + const int value; + }; + + struct TFlow {}; + struct TFunction {}; + struct TDelegate {}; + + // TODO: JP. this utility could be moved out of this header + //================================================================= + /* Creates Value for a type. For non-fundamental types + creates an empty Value object with the name of the type. + + If non-default initialization required for a non-fundamental, + create a template overload for your type. + */ + template + static choc::value::Value ValueFrom(const T& obj) + { + static constexpr bool is_string = std::is_same_v || std::is_same_v; + + if constexpr (std::is_same_v || (std::is_arithmetic_v && !std::is_unsigned_v) || is_string) + { + return choc::value::Value(obj); + } + else + { + // By default, custom types we store as choc Object with name of the type and a binary array of "data". + + static_assert(sizeof(T) >= sizeof(int32_t), "T must be larger than int32."); + static_assert(sizeof(T) % sizeof(int32_t) == 0, "Size of T must be a multiple of size of int32."); + + static constexpr auto dataSize = sizeof(T) / sizeof(int32_t); + + choc::value::Value valueObject = choc::value::createObject(type::type_name()); + + // We need to store data as choc Array instead of Vector to keep to/from YAML serialization generic + // for choc Value and choc Array can handle any type, not just primitives. + const auto* data = reinterpret_cast(&obj); + valueObject.addMember("Data", choc::value::createArray((uint32_t)dataSize, [&data](uint32_t i) { return data[i]; })); + + return valueObject; + } + } + + template<> choc::value::Value ValueFrom(const TFlow& obj) { return choc::value::createObject(type::type_name()); } + template<> choc::value::Value ValueFrom(const TFunction& obj) { return choc::value::createObject(type::type_name()); } + template<> choc::value::Value ValueFrom(const TDelegate& obj) { return choc::value::createObject(type::type_name()); } + + template<> choc::value::Value ValueFrom(const glm::vec3& obj) { + return choc::value::createObject(type::type_name(), "x", obj.x, "y", obj.y, "z", obj.z); + } + + //? not used (see Utils::CreateAssetHandle<>()) for creation of values from asset handles + template<> choc::value::Value ValueFrom(const UUID& obj) + { + choc::value::Value valueObject = choc::value::createObject(type::type_name()); + valueObject.addMember("Value", choc::value::Value((int64_t)obj)); + return valueObject; + } + + // TODO: JP. this utility could be moved out of this header + //================================================================= + /* Read custom data type created with ValueFrom() function. + + @returns std::nullopt if the 'customValueObject' was not created + with ValueFrom() function. + */ + template + static std::optional CustomValueTo(choc::value::ValueView customValueObject) + { + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) + { + if (customValueObject.isObjectWithClassName(type::type_name())) + { + return T(); + } + return {}; + } + else if constexpr (std::is_same_v) + { + if (customValueObject.isObjectWithClassName(type::type_name())) + { + return glm::vec3 { customValueObject["x"], customValueObject["y"], customValueObject["z"] }; + } + return {}; + } + if (customValueObject.isObjectWithClassName(type::type_name())) + { + return *(T*)(customValueObject["Data"].getRawData()); + } + return {}; + } + + //================================================================= + /** + Pin base. Using 'int typeID' for type-erased implementation + specific enum types. + */ + struct Pin + { + // Required Base members + uint64_t ID; + uint64_t NodeID; + std::string Name; + choc::value::Value Value; // Value should be the only thing to serialize, the rest is constructed from factory (TBD) + + StorageKind Storage; + PinKind Kind; + + std::optional*> EnumTokens{}; // TODO: replace with magic_enum utilities + + //! Pin type is erased from the base class and handled by the implementation + + bool IsSameType(const Pin* other) const { return GetType() == other->GetType(); }; + bool IsType(int typeID) const { return GetType() == typeID; }; + + protected: + Pin(uint64_t id, + std::string_view name, + StorageKind storageKind = StorageKind::Value, + choc::value::Value defaultValue = choc::value::Value() + ) + : ID(id) + , NodeID(0) + , Name(name) + , Value(defaultValue) + , Storage(storageKind) + , Kind(PinKind::Input) + {} + + Pin() + : ID(0) + , NodeID(0) + , Name("") + , Value(choc::value::Value()) + , Storage(StorageKind::Value) + , Kind(PinKind::Input) + {} + + public: + virtual ~Pin() = default; + + virtual int GetType() const = 0; + virtual std::string_view GetTypeString() const = 0; + }; + + //================================================================= + /* Optional utility to handle Pin types statically + */ + template + struct PinDescr : Pin + { + static constexpr auto pin_type = EType; + static constexpr auto color = Color; + using value_type = TValueType; + + /*constexpr*/ explicit PinDescr(const value_type& value) + //: DefaultValue(value) + { + Value = ValueFrom(value); + } +#if 0 + const value_type DefaultValue; +#endif + virtual int GetType() const override { return pin_type; } + virtual std::string_view GetTypeString() const override { return magic_enum::enum_name((ETypeEnum)EType); } + + //virtual ImU32 GetColor() const override { return color; } + }; + + template + struct is_matching_pin_type { static constexpr bool value = TPin::pin_type == EType; }; + + template + static constexpr bool is_matching_pin_type_v = TPin::pin_type == EType; + + using TDefaultPinTypes = std::tuple + < + PinDescr, + PinDescr, + PinDescr, + PinDescr, + PinDescr, + PinDescr, + PinDescr, + PinDescr, + PinDescr + >; + + // Immutable type doesn't allow changing type on the fly, + // it requires creating new pin of the new type to replace the old one + + // Example of how you could define default values for pin types +#if 0 + static const TDefaultPinTypes s_MyPinDefaultValues + { + TFlow{}, + true, + 5, + 3.0f, + "My string value", + 418ull, + TFunction{}, + TDelegate{}, + 9 + }; +#endif + + //================================================================= + /// Custom Pin types declaration + template + using TPinType = std::tuple_element_t; + + // This is how you could declare graph specific pin types for + // a variety of pin type utilities + /* + template + using TMyPinType = TPinType; + + TMyPinType myPin; + */ + + //================================================================= + struct Node + { + UUID ID; + std::string Category, Name, Description; + std::vector Inputs; // TODO: JP. these Ins and Outs can be assigned by the subclass + std::vector Outputs; + ImColor Color; + NodeType Type; // TODO: JP. this could also be implmentation specific types + ImVec2 Size; + + std::string State; + std::string SavedState; + + int32_t SortIndex; + static constexpr int32_t UndefinedSortIndex = -1; + + Node(std::string_view name, ImColor color = ImColor(255, 255, 255)) + : ID() + , Name(name) + , Color(color) + , Type(NodeType::Blueprint) + , Size(0, 0) + , SortIndex(UndefinedSortIndex) + { + } + + Node() = default; + + virtual ~Node() + { + for (auto* in : Inputs) + delete in; + + for (auto* out : Outputs) + delete out; + } + + bool operator==(const Node& other) const + { + return ID == other.ID + && Category == other.Category + && Name == other.Name + && GetTypeID() == other.GetTypeID(); + } + + // not used in the base graph context yet + virtual int GetTypeID() const { return TypeID; }; + + ImVec2 GetPosition() const; + void SetPosition(ImVec2 pos); + + protected: + int TypeID = -1; // Implementation specific type identifier (if required) + }; + + // Example +#if 0 + //================================================================= + /// Example of custom static Node type declaration + template + struct MyNode : Node + { + using TInputs = TInputsList; + using TOutputs = TOutputsList; + using TTopology = MyNode; + + virtual int GetTypeID() const override { return 0; } + + constexpr MyNode(TInputs&& inputs, TOutputs&& outputs) + : Ins(std::forward(inputs)) + , Outs(std::forward(outputs)) + {} + + TInputs Ins; + TOutputs Outs; + + constexpr auto GetIn(size_t index) const { return std::get(Ins); } + constexpr auto GetOut(size_t index) const { return std::get(Outs); } + + constexpr size_t GetInputCount() const { return std::tuple_size_v; } + constexpr size_t GetOutputCount() const { return std::tuple_size_v; } + }; + + +#define INPUT_LIST(...) __VA_ARGS__ +#define OUTPUT_LIST(...) __VA_ARGS__ + +#define DECLARE_GRAPH_NODE(NodeType, Inputs, Outputs) \ + struct NodeType : MyNode \ + { \ + using TTopology::TTopology; \ + \ + [[nodiscard]] static Node* Construct() \ + { \ + return static_cast(new NodeType(std::tuple(Inputs), std::tuple(Outputs))); \ + } \ + }; + + DECLARE_GRAPH_NODE(MyNodeType, + INPUT_LIST( + TMyPinType(5.0f), + TMyPinType(2) + ), + OUTPUT_LIST( + TMyPinType(3), + TMyPinType(true) + )); +#endif + + struct Link + { + UUID ID; + + UUID StartPinID; + UUID EndPinID; + + ImColor Color; + + Link(UUID startPinId, UUID endPinId) : + ID(), StartPinID(startPinId), EndPinID(endPinId), Color(255, 255, 255) + { + } + }; + + + //================================================================= + /// Node factories + + namespace Nodes { + using Registry = std::map>>; + using EnumTokensRegistry = std::unordered_map*>; + + class AbstractFactory + { + public: + virtual ~AbstractFactory() = default; + + virtual Node* SpawnNode(const std::string& category, const std::string& type) const = 0; + virtual Pin* CreatePinForType(int type) const = 0; + virtual ImColor GetIconColor(int pinTypeID) const = 0; + virtual ImColor GetValueColor(const choc::value::Value& v) const = 0; + virtual std::pair GetPinTypeAndStorageKindForValue(const choc::value::Value& v) const = 0; + }; + + template + class Factory : public AbstractFactory + { + public: + static const Registry Registry; + static const EnumTokensRegistry EnumTokensRegistry; + + Pin* CreatePinForType(int type) const override { return TDerivedFactory::Types::CreatePinForType(type); } + + ImColor GetIconColor(int pinTypeID) const override { return GetIconColorStatic(pinTypeID); } + ImColor GetValueColor(const choc::value::Value& v) const override { return GetValueColorStatic(v); } + std::pair GetPinTypeAndStorageKindForValue(const choc::value::Value& v) const override { return GetPinTypeAndStorageKindForValueStatic(v); } + + static ImColor GetIconColorStatic(int pinTypeID) { return TDerivedFactory::Types::GetPinColor(pinTypeID); } + static ImColor GetValueColorStatic(const choc::value::Value& v) + { + const auto pinType = TDerivedFactory::Types::GetPinTypeForValue(v); + return TDerivedFactory::Types::GetPinColor(pinType); + } + + static std::pair GetPinTypeAndStorageKindForValueStatic(const choc::value::Value& v) + { + const auto pinType = TDerivedFactory::Types::GetPinTypeForValue(v); + const bool isArray = v.isArray(); + return { pinType, isArray ? StorageKind::Array : StorageKind::Value }; + } + + + [[nodiscard]] static Node* SpawnNodeStatic(std::string_view category, std::string_view type) + { + auto cat = Registry.find(std::string(category)); + if (cat != Registry.end()) + { + auto nodes = cat->second.find(std::string(type)); + if (nodes != cat->second.end()) + { + Node* node = nodes->second(); + node->Category = category; + return node; + } + else + { + SE_CORE_ERROR("SpawnNodeStatic() - Category {0} does not contain node type {1}", category, type); + return nullptr; + } + } + + SE_CORE_ERROR("SpawnNodeStatic() - Category {0} does not exist", category); + return nullptr; +// return T::SpawnNodeStatic(); + } + + + // TODO: JP. utilize magic_enum + static const std::vector* GetEnumTokens(std::string_view nodeName, std::string_view memberName) + { + // Passed in names must not contain namespace + SE_CORE_ASSERT(nodeName.find("::") == std::string_view::npos); + SE_CORE_ASSERT(memberName.find("::") == std::string_view::npos); + + // TODO: for now this is specific to SoundGraph nodes + const std::string fullName = choc::text::template joinStrings>({ Utils::CreateUserFriendlyTypeName(nodeName), Utils::RemovePrefixAndSuffix(memberName) }, "::"); + if (!EnumTokensRegistry.count(fullName)) + return nullptr; + + return EnumTokensRegistry.at(fullName); + } + }; + + + inline std::string SanitizeMemberName(std::string_view name, bool removePrefixAndSuffix = false) + { + std::vector tokens = Utils::SplitString(name, "::"); + SE_CORE_ASSERT(tokens.size() >= 2); + + std::array validTokens = { + *(tokens.rbegin() + 1), + removePrefixAndSuffix ? std::string(Utils::RemovePrefixAndSuffix(*tokens.rbegin())) : *tokens.rbegin() + }; + return choc::text::joinStrings(validTokens, "::"); + } + + } // namespace Nodes + +} // namespace Hazel diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/PropertySet.cpp b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/PropertySet.cpp new file mode 100644 index 00000000..8ba8b0ba --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/PropertySet.cpp @@ -0,0 +1,515 @@ +#include "sepch.h" +#include "PropertySet.h" + +#include "NodeGraphUtils.h" +#include "StarEngine/Utilities/StringUtils.h" +#include "StarEngine/Utilities/SerializationMacros.h" +#include "StarEngine/Utilities/YAMLSerializationHelpers.h" + +#include "choc/text/choc_StringUtilities.h" +#include "yaml-cpp/yaml.h" + +namespace StarEngine::Utils { + + PropertySet::PropertySet() = default; + PropertySet::~PropertySet() = default; + PropertySet::PropertySet(PropertySet&&) = default; + PropertySet& PropertySet::operator= (PropertySet&&) = default; + + PropertySet::PropertySet(const PropertySet& other) + { + if (other.properties != nullptr) + properties = std::make_unique>(*other.properties); + } + + PropertySet& PropertySet::operator= (const PropertySet& other) + { + if (other.properties != nullptr) + properties = std::make_unique>(*other.properties); + + return *this; + } + +#if 0 + static std::string PropertyToString(const PropertySet::Property& prop) + { + auto desc = choc::text::addDoubleQuotes(prop.name) + ": " + + "{" + + choc::text::addDoubleQuotes("Type") + ": " + choc::text::addDoubleQuotes(std::string(GetTypeName(prop.value))) + ", " + + choc::text::addDoubleQuotes("Value") + ": "; + + return desc + choc::json::toString(prop.value) + "}"; + + //const auto& type = prop.value.getType(); + + ////assert(type.isPrimitive() || type.isString()); + + //if (type.isString()) + // return desc + choc::json::getEscapedQuotedString(prop.value.getString()); + + //if (type.isFloat()) return desc + choc::json::doubleToString(prop.value.getFloat64()); + //if (type.isInt()) return desc + std::to_string(prop.value.getInt64()); + + //return desc + prop.value.getDescription(); + } + + // TODO: this is taken from soul (ISC), move to some sort of utility header + // Join array of object with a function to stringify each element + template + static std::string JoinStrings(const Array& strings, std::string_view separator, StringifyFn&& stringify) + { + if (strings.empty()) + return {}; + + std::string s(stringify(strings.front())); + + for (size_t i = 1; i < strings.size(); ++i) + { + s += separator; + s += stringify(strings[i]); + } + + return s; + } + + static std::string PropertySetToString(const std::vector* properties) + { + if (properties == nullptr || properties->empty()) + return {}; + + auto content = JoinStrings(*properties, ", ", [&](auto& p) { return PropertyToString(p); }); + + return "{ " + content + " }"; + } +#endif + + bool PropertySet::IsEmpty() const { return Size() == 0; } + size_t PropertySet::Size() const { return properties == nullptr ? 0 : properties->size(); } + + choc::value::Value PropertySet::GetValue(std::string_view name) const + { + SE_CORE_ASSERT(!name.empty()); + return GetValue(name, {}); + } + + choc::value::Value PropertySet::GetValue(std::string_view name, const choc::value::Value& defaultReturnValue) const + { + SE_CORE_ASSERT(!name.empty()); + + if (properties != nullptr) + for (auto& p : *properties) + if (p.name == name) + return p.value; + + return defaultReturnValue; + } + + bool PropertySet::HasValue(std::string_view name) const + { + SE_CORE_ASSERT(!name.empty()); + + if (properties != nullptr) + for (auto& p : *properties) + if (p.name == name) // TODO: this should be replaced with faster option, like hash, or string ptr + return true; + + return false; + } + + bool PropertySet::GetBool(std::string_view name, bool defaultValue) const + { + auto v = GetValue(name); + return !v.isVoid() ? v.getView().get() : defaultValue; + } + + float PropertySet::GetFloat(std::string_view name, float defaultValue) const + { + auto v = GetValue(name); + return v.getType().isFloat() || v.getType().isInt() ? v.getView().get() : defaultValue; + } + + double PropertySet::GetDouble(std::string_view name, double defaultValue) const + { + auto v = GetValue(name); + return v.getType().isFloat() || v.getType().isInt() ? v.getView().get() : defaultValue; + } + + int32_t PropertySet::GetInt(std::string_view name, int32_t defaultValue) const + { + auto v = GetValue(name); + return v.getType().isFloat() || v.getType().isInt() ? v.getView().get() : defaultValue; + } + + int64_t PropertySet::GetInt64(std::string_view name, int64_t defaultValue) const + { + auto v = GetValue(name); + return v.getType().isFloat() || v.getType().isInt() ? v.getView().get() : defaultValue; + } + + std::string PropertySet::GetString(std::string_view name, std::string_view defaultValue) const + { + auto v = GetValue(name); + + if (!v.isVoid()) + { + return std::string(v.getView().get()); + + /*struct UnquotedPrinter : public soul::ValuePrinter + { + std::ostringstream out; + + void print(std::string_view s) override { out << s; } + void printStringLiteral(soul::StringDictionary::Handle h) override { print(dictionary->getStringForHandle(h)); } + }; + + UnquotedPrinter p; + p.dictionary = std::addressof(dictionary); + v.print(p); + return p.out.str();*/ + } + + return std::string(defaultValue); + } + + //static void replaceStringLiterals(choc::value::Value & v, soul::SubElementPath path, const soul::StringDictionary & sourceDictionary, soul::StringDictionary & destDictionary) + //{ + // auto value = v.getSubElement(path); + // + // if (value.getType().isStringLiteral()) + // { + // auto s = sourceDictionary.getStringForHandle(value.getStringLiteral()); + // v.modifySubElementInPlace(path, choc::value::Value::createStringLiteral(destDictionary.getHandleForString(s))); + // } + // else if (value.getType().isFixedSizeArray()) + // { + // for (size_t i = 0; i < value.getType().getArraySize(); ++i) + // replaceStringLiterals(v, path + i, sourceDictionary, destDictionary); + // } + // else if (value.getType().isStruct()) + // { + // for (size_t i = 0; i < value.getType().getStructRef().getNumMembers(); ++i) + // replaceStringLiterals(v, path + i, sourceDictionary, destDictionary); + // } + //} + + void PropertySet::SetInternal(std::string_view name, choc::value::Value newValue) + { + SE_CORE_ASSERT(!name.empty()); + + if (properties == nullptr) + { + properties = std::make_unique>(); + } + else + { + for (auto& p : *properties) + { + if (p.name == name) + { + p.value = std::move(newValue); + return; + } + } + } + + properties->push_back({ std::string(name), std::move(newValue) }); + } + + void PropertySet::Set(std::string_view name, choc::value::Value newValue) + { + //replaceStringLiterals(newValue, {}, sourceDictionary, dictionary); + SetInternal(name, std::move(newValue)); + } + + void PropertySet::Set(std::string_view name, int32_t value) { SetInternal(name, choc::value::Value(value)); } + void PropertySet::Set(std::string_view name, int64_t value) { SetInternal(name, choc::value::Value(value)); } + void PropertySet::Set(std::string_view name, float value) { SetInternal(name, choc::value::Value(value)); } + void PropertySet::Set(std::string_view name, double value) { SetInternal(name, choc::value::Value(value)); } + void PropertySet::Set(std::string_view name, bool value) { SetInternal(name, choc::value::Value(value)); } + void PropertySet::Set(std::string_view name, const char* value) { Set(name, std::string(value)); } + void PropertySet::Set(std::string_view name, std::string_view value) { SetInternal(name, choc::value::Value(value)); } + + void PropertySet::Set(std::string_view name, const choc::value::ValueView & value) + { + return Set(name, choc::value::Value(value)); + } + + // TODO: this is taken from soul (ISC), move to some sort of utility header + template + inline bool RemoveIf(Vector& v, Predicate&& pred) + { + auto oldEnd = std::end(v); + auto newEnd = std::remove_if(std::begin(v), oldEnd, pred); + + if (newEnd == oldEnd) + return false; + + v.erase(newEnd, oldEnd); + return true; + } + + void PropertySet::Remove(std::string_view name) + { + SE_CORE_ASSERT(!name.empty()); + + if (properties != nullptr) + RemoveIf(*properties, [&](const Property& p) { return p.name == name; }); + } + + std::vector PropertySet::GetNames() const + { + std::vector result; + + if (properties != nullptr) + { + result.reserve(properties->size()); + + for (auto& p : *properties) + result.push_back(p.name); + } + + return result; + } + +//choc::value::Value PropertySet::toExternalValue() const +//{ +// auto o = choc::value::createObject("PropertySet"); +// +// if (properties != nullptr) +// for (auto& p : *properties) +// o.addMember(p.name, p.value.toExternalValue(ConstantTable(), dictionary)); +// +// return o; +//} + + + template + void ParseValueOrArray(PropertySet& ps, std::string_view name, const choc::value::ValueView& value) + { + if (value["Value"].isArray()) + { + if constexpr (std::is_same_v) + { + //? array of strings might cause issues at some point + choc::value::Value valueArray = choc::value::createArray(value["Value"].size(), [&value](uint32_t i) { return value["Value"][i]; }); + ps.Set(std::string(name), std::move(valueArray)); + } + else + { + choc::value::Value valueArray = choc::value::createArray(value["Value"].size(), [&value](uint32_t i) { return value["Value"][i].get(); }); + ps.Set(std::string(name), std::move(valueArray)); + } + } + else + { + if constexpr (std::is_same_v) + ps.Set(std::string(name), choc::value::Value(value["Value"].getString())); + else + ps.Set(std::string(name), choc::value::Value(value["Value"].get())); + } + } + + // For backwards compatability. + // Audio asset handles used to be stored as int64_t. + // If we encounter these, we need to convert to new "AssetHandle" object. + // May have to remove this in future (if we want to use int64_t for other things) + template<> + void ParseValueOrArray(PropertySet& ps, std::string_view name, const choc::value::ValueView& value) + { + if (value["Value"].isArray()) + { + choc::value::Value valueArray = choc::value::createArray(value["Value"].size(), [&value](uint32_t i) + { + return choc::value::createObject( + "AudioAsset", + "Id", choc::value::createInt64(value["Value"][i].getInt64()) + ); + }); + ps.Set(std::string(name), std::move(valueArray)); + } + else + { + auto assetHandle = choc::value::createObject( + "AudioAsset", + "Id", choc::value::createInt64(value["Value"].getInt64()) + ); + ps.Set(std::string(name), std::move(assetHandle)); + } + } + + + void ParseAssetHandle(PropertySet& ps, std::string_view name, std::string_view type, const choc::value::ValueView& value) + { + if (value.isArray()) + { + choc::value::Value valueArray = choc::value::createArray(value.size(), [&type, &value](uint32_t i) + { + return choc::value::createObject( + type, + "Id", choc::value::createInt64(value[i].getInt64()) + ); + }); + ps.Set(std::string(name), std::move(valueArray)); + } + else + { + auto assetHandle = choc::value::createObject( + type, + "Id", choc::value::createInt64(value.getInt64()) + ); + ps.Set(std::string(name), std::move(assetHandle)); + } + } + + + choc::value::Value ParseCustomValueType(const choc::value::ValueView& value) + { + if (value.isVoid()) + { + SE_CORE_WARN("Failed to deserialize custom value type, value is not an object."); + return choc::value::Value(value); + } + if (value["TypeName"].isVoid()) + { + SE_CORE_ASSERT(false, "Failed to deserialize custom value type, missing \"TypeName\" property."); + return {}; + } + + choc::value::Value customObject = choc::value::createObject(value["TypeName"].get()); + if (value.isObject()) + { + for (uint32_t i = 0; i < value.size(); i++) + { + choc::value::MemberNameAndValue nameValue = value.getObjectMemberAt(i); + customObject.addMember(nameValue.name, nameValue.value); + } + } + else + { + SE_CORE_ASSERT(false, "Failed to load custom value type. It must be serialized as object.") + } + + return customObject; + }; + + void ParseCustomObject(PropertySet& ps, std::string_view name, const choc::value::ValueView& value) + { + if (value["Value"].isArray()) + { + // TODO: implement arrays of custom value types + SE_CORE_ASSERT(false); +#if 0 + choc::value::ValueView inArray = value["Value"]; + std::string type = value["Type"].get(); + const uint32_t inArraySize = inArray.size(); + + SE_CORE_ASSERT(type == "AssetHandle", "The only custom type is supported for now is \"AssetHandle\"."); + SE_CORE_ASSERT(inArraySize > 0); + + choc::value::Value valueArray = choc::value::createEmptyArray(); + + for (int i = 0; i < inArraySize; ++i) + { + choc::value::Value vobj = CreateAssetRefObject(); + vobj["Value"].set(inArray[i]["Value"].get()); + valueArray.addArrayElement(vobj); + } + SE_CORE_ASSERT(valueArray.size() == inArraySize) + + ps.Set(std::string(name), valueArray); + + auto et = ps.GetValue(std::string(name)).getType().getElementType(); +#endif + } + else + ps.Set(std::string(name), ParseCustomValueType(value["Value"])); + } + +#if 0 + PropertySet PropertySet::FromJSON(const choc::value::ValueView& v) + { + SE_CORE_ASSERT(false, "Should use ..ExternalValue() serialization instead."); + + //? old way of restoring from JSON string + + PropertySet a; + + //if (v.isObjectWithClassName(objectClassName /*"PropertySet"*/)) + { + v.visitObjectMembers([&](std::string_view name, const choc::value::ValueView& value) + { + // Need to store type as a property of each value object + // because .json can't distinguish between Int and Float if the value is 0.0, + // it just knows type "number" + if (value.isObject() && value.hasObjectMember("Type")) + { + std::string type(value["Type"].getString()); + + if (type == "Float") + ParseValueOrArray(a, name, value); + else if (type == "Int") + ParseValueOrArray(a, name, value); + else if (type == "Bool") + ParseValueOrArray(a, name, value); + else if (type == "AssetHandle") //? might not be desireable in all cases to reserve int64 to AssetHandles + ParseValueOrArray(a, name, value); + else if (type == "String") + ParseValueOrArray(a, name, value); + else + ParseCustomObject(a, name, value); + } + else + a.Set(std::string(name), value); + }); + } + + return a; + } + + std::string PropertySet::ToJSON() const { SE_CORE_ASSERT(false, "Should use ..ExternalValue() serialization instead."); return PropertySetToString(properties.get()); } +#endif + + choc::value::Value PropertySet::ToExternalValue() const + { + choc::value::Value out = choc::value::createObject("PropertySet"); + + if (properties) + { + for (const auto& prop : *properties.get()) + { + out.addMember(prop.name, prop.value); + } + } + + return out; + } + + PropertySet PropertySet::FromExternalValue(const choc::value::ValueView& deserialized) + { + PropertySet properties; + + if (!deserialized.isObjectWithClassName("PropertySet")) + return properties; + + for (decltype(deserialized.size()) i = 0; i < deserialized.size(); ++i) + { + const auto member = deserialized.getObjectMemberAt(i); + properties.Set(member.name, member.value); + } + + return properties; + } + + void PropertySet::Sort() + { + // sort case-insensitive + if (properties) + { + std::sort(properties->begin(), properties->end(), [](const Property& a, const Property& b) -> bool + { + return Utils::String::CompareCase(a.name, b.name) < 0; + }); + } + } + +} // namespace Hazel::Utils diff --git a/StarEngine/src/StarEngine/Editor/NodeGraphEditor/PropertySet.h b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/PropertySet.h new file mode 100644 index 00000000..c166ffaa --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/NodeGraphEditor/PropertySet.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +namespace StarEngine::Utils { + + //============================================================================== + /** A set of named properties. The value of each item is a Value object. */ + struct PropertySet + { + PropertySet(); + ~PropertySet(); + PropertySet(const PropertySet&); + PropertySet(PropertySet&&); + PropertySet& operator= (const PropertySet&); + PropertySet& operator= (PropertySet&&); + + struct Property + { + std::string name; + choc::value::Value value; + }; + + bool IsEmpty() const; + size_t Size() const; + + // TODO: JP. maybe we should return ValueView, or have a version that returns ValueView? + choc::value::Value GetValue(std::string_view name) const; + choc::value::Value GetValue(std::string_view name, const choc::value::Value& defaultReturnValue) const; + bool HasValue(std::string_view name) const; + + bool GetBool(std::string_view name, bool defaultValue = false) const; + float GetFloat(std::string_view name, float defaultValue = 0.0f) const; + double GetDouble(std::string_view name, double defaultValue = 0.0) const; + int32_t GetInt(std::string_view name, int32_t defaultValue = 0) const; + int64_t GetInt64(std::string_view name, int64_t defaultValue = 0) const; + std::string GetString(std::string_view name, std::string_view defaultValue = {}) const; + + void Set(std::string_view name, int32_t value); + void Set(std::string_view name, int64_t value); + void Set(std::string_view name, float value); + void Set(std::string_view name, double value); + void Set(std::string_view name, bool value); + void Set(std::string_view name, const char* value); + void Set(std::string_view name, std::string_view value); + void Set(std::string_view name, choc::value::Value newValue); + void Set(std::string_view name, const choc::value::ValueView& value); + + void Remove(std::string_view name); + + std::vector GetNames() const; + + choc::value::Value ToExternalValue() const; + static PropertySet FromExternalValue(const choc::value::ValueView& deserialized); + + // Sort property set by name (case insensitive) + void Sort(); + +#if 0 + //? DEPRECATED + std::string ToJSON() const; + //? DEPRECATED + static PropertySet FromJSON(const choc::value::ValueView& parsedJSON); +#endif + + private: + std::unique_ptr> properties; + + void Set(std::string_view name, const void* value) = delete; + void SetInternal(std::string_view name, choc::value::Value newValue); + }; + +} // namespace hazel::Utils diff --git a/StarEngine/src/StarEngine/Editor/PanelManager.cpp b/StarEngine/src/StarEngine/Editor/PanelManager.cpp new file mode 100644 index 00000000..38e3544d --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/PanelManager.cpp @@ -0,0 +1,162 @@ +#include "sepch.h" +#include "PanelManager.h" +#include "StarEngine/Utilities/FileSystem.h" + +#include + +namespace StarEngine { + + PanelManager::~PanelManager() + { + for (auto& map : m_Panels) + map.clear(); + } + + PanelData* PanelManager::GetPanelData(uint32_t panelID) + { + for (auto& panelMap : m_Panels) + { + if (panelMap.find(panelID) == panelMap.end()) + continue; + + return &panelMap.at(panelID); + } + + return nullptr; + } + + const PanelData* PanelManager::GetPanelData(uint32_t panelID) const + { + for (auto& panelMap : m_Panels) + { + if (panelMap.find(panelID) == panelMap.end()) + continue; + + return &panelMap.at(panelID); + } + + return nullptr; + } + + void PanelManager::RemovePanel(const char* strID) + { + uint32_t id = Hash::GenerateFNVHash(strID); + for (auto& panelMap : m_Panels) + { + if (panelMap.find(id) == panelMap.end()) + continue; + + panelMap.erase(id); + return; + } + + SE_CORE_ERROR_TAG("PanelManager", "Couldn't find panel with id '{0}'", strID); + } + + void PanelManager::OnImGuiRender() + { + for (auto& panelMap : m_Panels) + { + for (auto& [id, panelData] : panelMap) + { + bool closedThisFrame = false; + + if (panelData.IsOpen) + { + panelData.Panel->OnImGuiRender(panelData.IsOpen); + closedThisFrame = !panelData.IsOpen; + } + + if (closedThisFrame) + Serialize(); + } + } + } + + void PanelManager::OnEvent(Event& e) + { + for (auto& panelMap : m_Panels) + { + for (auto& [id, panelData] : panelMap) + panelData.Panel->OnEvent(e); + } + } + + void PanelManager::SetSceneContext(const Ref& context) + { + for (auto& panelMap : m_Panels) + { + for (auto& [id, panelData] : panelMap) + panelData.Panel->SetSceneContext(context); + } + } + + void PanelManager::OnProjectChanged(const Ref& project) + { + for (auto& panelMap : m_Panels) + { + for (auto& [id, panelData] : panelMap) + panelData.Panel->OnProjectChanged(project); + } + + Deserialize(); + } + + void PanelManager::Serialize() const + { + YAML::Emitter out; + out << YAML::BeginMap; + + out << YAML::Key << "Panels" << YAML::Value << YAML::BeginSeq; + { + for (size_t category = 0; category < m_Panels.size(); category++) + { + for (const auto& [panelID, panel] : m_Panels[category]) + { + out << YAML::BeginMap; + out << YAML::Key << "ID" << YAML::Value << panelID; + out << YAML::Key << "Name" << YAML::Value << panel.Name; + out << YAML::Key << "IsOpen" << YAML::Value << panel.IsOpen; + out << YAML::EndMap; + } + } + } + out << YAML::EndSeq; + + out << YAML::EndMap; + + std::ofstream fout(FileSystem::GetPersistentStoragePath() / "EditorLayout.yaml"); + fout << out.c_str(); + fout.close(); + } + + void PanelManager::Deserialize() + { + std::filesystem::path layoutPath = FileSystem::GetPersistentStoragePath() / "EditorLayout.yaml"; + if (!FileSystem::Exists(layoutPath)) + return; + + std::ifstream stream(layoutPath); + SE_CORE_VERIFY(stream); + std::stringstream ss; + ss << stream.rdbuf(); + + YAML::Node data = YAML::Load(ss.str()); + if (!data["Panels"]) + { + SE_CONSOLE_LOG_ERROR("Failed to load EditorLayout.yaml from {} because the file appears to be corrupted!", layoutPath.parent_path().string()); + return; + } + + for (auto panelNode : data["Panels"]) + { + PanelData* panelData = GetPanelData(panelNode["ID"].as(0)); + + if (panelData == nullptr) + continue; + + panelData->IsOpen = panelNode["IsOpen"].as(panelData->IsOpen); + } + } + +} diff --git a/StarEngine/src/StarEngine/Editor/PanelManager.h b/StarEngine/src/StarEngine/Editor/PanelManager.h new file mode 100644 index 00000000..140cba07 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/PanelManager.h @@ -0,0 +1,106 @@ +#pragma once + +#include "StarEngine/Core/Base.h" +#include "StarEngine/Core/Log.h" +#include "StarEngine/Core/Hash.h" + +#include "EditorPanel.h" + +#include +#include + +namespace StarEngine { + + struct PanelData + { + const char* ID = ""; + const char* Name = ""; + Ref Panel = nullptr; + bool IsOpen = false; + }; + + // NOTE(Peter): For now this just corresponds to what menu the panel will be listed in (in Hazelnut) + enum class PanelCategory + { + Edit, View, _COUNT + }; + + class PanelManager + { + public: + PanelManager() = default; + ~PanelManager(); + + PanelData* GetPanelData(uint32_t panelID); + const PanelData* GetPanelData(uint32_t panelID) const; + + void RemovePanel(const char* strID); + + void OnImGuiRender(); + + void OnEvent(Event& e); + + void SetSceneContext(const Ref& context); + void OnProjectChanged(const Ref& project); + + void Serialize() const; + void Deserialize(); + + std::unordered_map& GetPanels(PanelCategory category) { return m_Panels[(size_t)category]; } + const std::unordered_map& GetPanels(PanelCategory category) const { return m_Panels[(size_t)category]; } + + public: + template + Ref AddPanel(PanelCategory category, const PanelData& panelData) + { + static_assert(std::is_base_of::value, "PanelManager::AddPanel requires TPanel to inherit from EditorPanel"); + + auto& panelMap = m_Panels[(size_t)category]; + + uint32_t id = Hash::GenerateFNVHash(panelData.ID); + if (panelMap.find(id) != panelMap.end()) + { + SE_CORE_ERROR_TAG("PanelManager", "A panel with the id '{0}' has already been added.", panelData.ID); + return nullptr; + } + + panelMap[id] = panelData; + return panelData.Panel.As(); + } + + template + Ref AddPanel(PanelCategory category, const char* strID, bool isOpenByDefault, TArgs&&... args) + { + return AddPanel(category, PanelData{ strID, strID, Ref::Create(std::forward(args)...), isOpenByDefault }); + } + + template + Ref AddPanel(PanelCategory category, const char* strID, const char* displayName, bool isOpenByDefault, TArgs&&... args) + { + return AddPanel(category, PanelData{ strID, displayName, Ref::Create(std::forward(args)...), isOpenByDefault }); + } + + template + Ref GetPanel(const char* strID) + { + static_assert(std::is_base_of::value, "PanelManager::AddPanel requires TPanel to inherit from EditorPanel"); + + uint32_t id = Hash::GenerateFNVHash(strID); + + for (const auto& panelMap : m_Panels) + { + if (panelMap.find(id) == panelMap.end()) + continue; + + return panelMap.at(id).Panel.As(); + } + + SE_CORE_ERROR_TAG("PanelManager", "Couldn't find panel with id '{0}'", strID); + return nullptr; + } + + private: + std::array, (size_t)PanelCategory::_COUNT> m_Panels; + }; + +} diff --git a/StarEngine/src/StarEngine/Editor/SceneHierarchyPanel.cpp b/StarEngine/src/StarEngine/Editor/SceneHierarchyPanel.cpp new file mode 100644 index 00000000..8c3c8b42 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/SceneHierarchyPanel.cpp @@ -0,0 +1,3825 @@ +#include "sepch.h" +#include "SceneHierarchyPanel.h" + +#include "StarEngine/Asset/AssetManager.h" +//#include "StarEngine/Audio/AudioComponent.h" +#include "StarEngine/Audio/AudioEngine.h" +#include "StarEngine/Core/Events/SceneEvents.h" +#include "StarEngine/Core/Input.h" + +#include "StarEngine/Debug/Profiler.h" + +#include "StarEngine/Editor/EditorApplicationSettings.h" +//#include "StarEngine/Editor/NodeGraphEditor/AnimationGraph/AnimationGraphEditor.h" +#include "StarEngine/ImGui/CustomTreeNode.h" +#include "StarEngine/ImGui/PropertyGrid.h" +#include "StarEngine/ImGui/ImGuiWidgets.h" +#include "StarEngine/Physics/PhysicsLayer.h" +#include "StarEngine/Physics/PhysicsScene.h" +#include "StarEngine/Physics/PhysicsSystem.h" +#include "StarEngine/Scripting/ScriptEngine.h" +//#include "StarEngine/Scripting/ScriptAsset.h" +#include "StarEngine/Renderer/MeshFactory.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Renderer/UI/Font.h" +#include "StarEngine/Scene/Prefab.h" +#include "StarEngine/Utilities/AnimationUtils.h" + +#include +#include +#include + +using namespace magic_enum::bitwise_operators; // out-of-the-box bitwise operators for enums. + +namespace StarEngine { + + static ImRect s_WindowBounds; + static bool s_ActivateSearchWidget = false; + + SelectionContext SceneHierarchyPanel::s_ActiveSelectionContext = SelectionContext::Scene; + + SceneHierarchyPanel::SceneHierarchyPanel(const Ref& context, SelectionContext selectionContext, bool isWindow) + : m_Context(context), m_SelectionContext(selectionContext), m_IsWindow(isWindow) + { + if (m_Context) + m_Context->SetEntityDestroyedCallback([this](Entity entity) { OnExternalEntityDestroyed(entity); }); + + m_ComponentCopyScene = Scene::CreateEmpty(); + m_ComponentCopyEntity = m_ComponentCopyScene->CreateEntity(); + } + + void SceneHierarchyPanel::SetSceneContext(const Ref& scene) + { + m_Context = scene; + if (m_Context) + m_Context->SetEntityDestroyedCallback([this](Entity entity) { OnExternalEntityDestroyed(entity); }); + } + + void SceneHierarchyPanel::OnImGuiRender(bool& isOpen) + { + SE_PROFILE_FUNCTION("SceneHierarchyPanel::OnImGuiRender"); + if (m_IsWindow) + { + UI::ScopedStyle padding(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin("Scene Hierarchy", &isOpen); + } + + s_ActiveSelectionContext = m_SelectionContext; + + m_IsWindowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + + ImRect windowRect = { ImGui::GetWindowContentRegionMin(), ImGui::GetWindowContentRegionMax() }; + + { + const float edgeOffset = 4.0f; + UI::ShiftCursorX(edgeOffset * 3.0f); + UI::ShiftCursorY(edgeOffset * 2.0f); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - edgeOffset * 3.0f); + + static std::string searchedString; + + if (s_ActivateSearchWidget) + { + ImGui::SetKeyboardFocusHere(); + s_ActivateSearchWidget = false; + } + + UI::Widgets::SearchWidget(searchedString); + + ImGui::Spacing(); + ImGui::Spacing(); + + // Entity list + //------------ + + UI::ScopedStyle cellPadding(ImGuiStyleVar_CellPadding, ImVec2(4.0f, 0.0f)); + + // Alt row colour + const ImU32 colRowAlt = UI::ColourWithMultipliedValue(Colors::Theme::backgroundDark, 1.3f); + UI::ScopedColour tableBGAlt(ImGuiCol_TableRowBgAlt, colRowAlt); + + // Table + { + // Scrollable Table uses child window internally + UI::ScopedColour tableBg(ImGuiCol_ChildBg, Colors::Theme::backgroundDark); + + ImGuiTableFlags tableFlags = ImGuiTableFlags_NoPadInnerX + | ImGuiTableFlags_Resizable + | ImGuiTableFlags_Reorderable + | ImGuiTableFlags_ScrollY + /*| ImGuiTableFlags_RowBg *//*| ImGuiTableFlags_Sortable*/; + + const int numColumns = 3; + if (ImGui::BeginTable("##SceneHierarchy-Table", numColumns, tableFlags, ImVec2(ImGui::GetContentRegionAvail()))) + { + + ImGui::TableSetupColumn("Label"); + ImGui::TableSetupColumn("Type"); + ImGui::TableSetupColumn("Visibility"); + + // Headers + { + const ImU32 colActive = UI::ColourWithMultipliedValue(Colors::Theme::groupHeader, 1.2f); + UI::ScopedColourStack headerColours(ImGuiCol_HeaderHovered, colActive, + ImGuiCol_HeaderActive, colActive); + + ImGui::TableSetupScrollFreeze(ImGui::TableGetColumnCount(), 1); + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers, 22.0f); + for (int column = 0; column < ImGui::TableGetColumnCount(); column++) + { + ImGui::TableSetColumnIndex(column); + const char* column_name = ImGui::TableGetColumnName(column); + UI::ScopedID columnID(column); + + UI::ShiftCursor(edgeOffset * 3.0f, edgeOffset * 2.0f); + ImGui::TableHeader(column_name); + UI::ShiftCursor(-edgeOffset * 3.0f, -edgeOffset * 2.0f); + } + ImGui::SetCursorPosX(ImGui::GetCurrentTable()->OuterRect.Min.x); + UI::Draw::Underline(true, 0.0f, 5.0f); + } + + // List + { + UI::ScopedColourStack entitySelection(ImGuiCol_Header, IM_COL32_DISABLE, + ImGuiCol_HeaderHovered, IM_COL32_DISABLE, + ImGuiCol_HeaderActive, IM_COL32_DISABLE); + + for (auto entity : m_Context->GetAllEntitiesWith()) + { + Entity e(entity, m_Context.Raw()); + if (e.GetParentUUID() == 0) + DrawEntityNode({ entity, m_Context.Raw() }, searchedString); + } + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Spacing(); + ImGui::Dummy(ImVec2(0, 50.0f)); + + if (ImGui::BeginPopupContextWindow(nullptr, ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems)) + { + DrawEntityCreateMenu({}); + ImGui::EndPopup(); + } + + + ImGui::EndTable(); + } + } + + s_WindowBounds = ImGui::GetCurrentWindow()->Rect(); + } + + if (ImGui::BeginDragDropTargetCustom(windowRect, ImGui::GetCurrentWindow()->ID)) + { + const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("scene_entity_hierarchy", ImGuiDragDropFlags_AcceptNoDrawDefaultRect); + + if (payload) + { + size_t count = payload->DataSize / sizeof(UUID); + + bool canDrop = true; + for (size_t i = 0; i < count; i++) + { + UUID entityID = *(((UUID*)payload->Data) + i); + Entity entity = m_Context->GetEntityWithUUID(entityID); + if (entity.HasComponent()) + { + canDrop = false; + break; + } + } + + if (canDrop) + { + for (size_t i = 0; i < count; i++) + { + UUID entityID = *(((UUID*)payload->Data) + i); + Entity entity = m_Context->GetEntityWithUUID(entityID); + m_Context->UnparentEntity(entity); + } + } + } + + ImGui::EndDragDropTarget(); + } + + { + UI::ScopedStyle windowPadding(ImGuiStyleVar_WindowPadding, ImVec2(2.0, 4.0f)); + ImGui::Begin("Properties"); + m_IsHierarchyOrPropertiesFocused = m_IsWindowFocused || ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + DrawComponents(SelectionManager::GetSelections(s_ActiveSelectionContext)); + ImGui::End(); + } + + if (m_IsWindow) + ImGui::End(); + } + + void SceneHierarchyPanel::OnEvent(Event& event) + { + if (!m_IsWindowFocused) + return; + + EventDispatcher dispatcher(event); + dispatcher.Dispatch([&](MouseButtonReleasedEvent& e) + { + if (ImGui::IsMouseHoveringRect(s_WindowBounds.Min, s_WindowBounds.Max, false) && !ImGui::IsAnyItemHovered()) + { + m_FirstSelectedRow = -1; + m_LastSelectedRow = -1; + SelectionManager::DeselectAll(); + return true; + } + + return false; + }); + + dispatcher.Dispatch([&](KeyPressedEvent& e) + { + if (!m_IsWindowFocused) + return false; + + switch (e.GetKeyCode()) + { + case KeyCode::F: + { + s_ActivateSearchWidget = true; + return true; + } + case KeyCode::Escape: + { + m_FirstSelectedRow = -1; + m_LastSelectedRow = -1; + break; + } + } + + return false; + }); + } + + void SceneHierarchyPanel::DrawEntityCreateMenu(Entity parent) + { + if (!ImGui::BeginMenu("Create")) + return; + + Entity newEntity; + + if (ImGui::MenuItem("Empty Entity")) + { + newEntity = m_Context->CreateEntity("Empty Entity"); + } + + if (ImGui::BeginMenu("Camera")) + { + if (ImGui::MenuItem("From View")) + { + newEntity = m_Context->CreateEntity("Camera"); + newEntity.AddComponent(); + + for (auto& func : m_EntityContextMenuPlugins) + func(newEntity); + } + + if (ImGui::MenuItem("At World Origin")) + { + newEntity = m_Context->CreateEntity("Camera"); + newEntity.AddComponent(); + } + + ImGui::EndMenu(); + } + + if (ImGui::MenuItem("Text")) + { + newEntity = m_Context->CreateEntity("Text"); + auto& textComp = newEntity.AddComponent(); + textComp.FontHandle = Font::GetDefaultFont()->Handle; + } + + if (ImGui::MenuItem("Sprite")) + { + newEntity = m_Context->CreateEntity("Sprite"); + auto& spriteComp = newEntity.AddComponent(); + } + + if (ImGui::BeginMenu("3D")) + { + auto create3DEntity = [this](const char* entityName, const char* targetAssetName, const char* sourceAssetName) + { + Entity entity = m_Context->CreateEntity(entityName); + std::filesystem::path sourcePath = "Meshes/Source/Default"; + std::filesystem::path targetPath = "Meshes/Default"; + auto mesh = Project::GetEditorAssetManager()->GetAssetHandleFromFilePath(targetPath / targetAssetName); + if (mesh != 0) + { + entity.AddComponent(mesh); + } + else + { + if (std::filesystem::exists(Project::GetActiveAssetDirectory() / sourcePath / sourceAssetName)) + { + AssetHandle assetHandle = Project::GetEditorAssetManager()->GetAssetHandleFromFilePath(sourcePath / sourceAssetName); + if (AssetManager::GetAsset(assetHandle)) + { + Ref mesh = Project::GetEditorAssetManager()->CreateOrReplaceAsset(targetPath / targetAssetName, assetHandle, /*generateColliders=*/false); + entity.AddComponent(mesh->Handle); + } + } + else + SE_CONSOLE_LOG_WARN("Please import the default mesh source files to the following path: {0}", Project::GetActiveAssetDirectory() / sourcePath); + } + return entity; + }; + + if (ImGui::MenuItem("Cube")) + { + newEntity = create3DEntity("Cube", "Cube.hsmesh", "Cube.gltf"); + newEntity.AddComponent(); + } + + if (ImGui::MenuItem("Sphere")) + { + newEntity = create3DEntity("Sphere", "Sphere.hsmesh", "Sphere.gltf"); + newEntity.AddComponent(); + } + + if (ImGui::MenuItem("Capsule")) + { + newEntity = create3DEntity("Capsule", "Capsule.hsmesh", "Capsule.gltf"); + newEntity.AddComponent(); + } + + if (ImGui::MenuItem("Cylinder")) + { + newEntity = create3DEntity("Cylinder", "Cylinder.hsmesh", "Cylinder.gltf"); + newEntity.AddComponent(); + PhysicsSystem::GetOrCreateColliderAsset(newEntity, newEntity.GetComponent()); + } + + if (ImGui::MenuItem("Torus")) + { + newEntity = create3DEntity("Torus", "Torus.hsmesh", "Torus.gltf"); + newEntity.AddComponent(); + PhysicsSystem::GetOrCreateColliderAsset(newEntity, newEntity.GetComponent()); + } + + if (ImGui::MenuItem("Plane")) + { + newEntity = create3DEntity("Plane", "Plane.hsmesh", "Plane.gltf"); + + // A mesh collider won't work for a plane since it has zero height. + // This leads to crashes in the Physics system. + // We have two choices. Either: + // * Don't create any collider at all + // * Create a box collider with a small, non-zero height + // I've chosen the latter. + auto& boxCollider = newEntity.AddComponent(); + boxCollider.HalfSize = glm::vec3(0.5f, 0.02f, 0.5f); // 0.02 is smallest the height can be before Jolt Physics gives up. + boxCollider.Offset = glm::vec3(0.0f, -0.02f, 0.0f); + } + + if (ImGui::MenuItem("Cone")) + { + newEntity = create3DEntity("Cone", "Cone.hsmesh", "Cone.gltf"); + newEntity.AddComponent(); + PhysicsSystem::GetOrCreateColliderAsset(newEntity, newEntity.GetComponent()); + } + + ImGui::EndMenu(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Directional Light")) + { + newEntity = m_Context->CreateEntity("Directional Light"); + newEntity.AddComponent(); + newEntity.GetComponent().SetRotationEuler(glm::radians(glm::vec3{80.0f, 10.0f, 0.0f})); + } + + if (ImGui::MenuItem("Point Light")) + { + newEntity = m_Context->CreateEntity("Point Light"); + newEntity.AddComponent(); + } + + if (ImGui::MenuItem("Spot Light")) + { + newEntity = m_Context->CreateEntity("Spot Light"); + newEntity.AddComponent(); + newEntity.GetComponent().Translation = glm::vec3{ 0 }; + newEntity.GetComponent().SetRotationEuler(glm::radians(glm::vec3{90.0f, 0.0f, 0.0f})); + } + + if (ImGui::MenuItem("Sky Light")) + { + newEntity = m_Context->CreateEntity("Sky Light"); + newEntity.AddComponent(); + } + + ImGui::Separator(); + /* + if (ImGui::MenuItem("Ambient Sound")) + { + newEntity = m_Context->CreateEntity("Ambient Sound"); + newEntity.AddComponent(); + }*/ + + if (newEntity) + { + if (parent) + { + m_Context->ParentEntity(newEntity, parent); + newEntity.Transform().Translation = glm::vec3(0.0f); + } + + SelectionManager::DeselectAll(); + SelectionManager::Select(s_ActiveSelectionContext, newEntity.GetUUID()); + } + + ImGui::EndMenu(); + } + + bool SceneHierarchyPanel::TagSearchRecursive(Entity entity, std::string_view searchFilter, uint32_t maxSearchDepth, uint32_t currentDepth) + { + if (searchFilter.empty()) + return false; + + for (auto child : entity.Children()) + { + Entity e = m_Context->GetEntityWithUUID(child); + if (e.HasComponent()) + { + if (UI::IsMatchingSearch(e.GetComponent().Tag, searchFilter)) + return true; + } + + bool found = TagSearchRecursive(e, searchFilter, maxSearchDepth, currentDepth + 1); + if (found) + return true; + } + return false; + } + + + void SceneHierarchyPanel::DrawEntityNode(Entity entity, const std::string& searchFilter) + { + const char* name = "Unnamed Entity"; + if (entity.HasComponent()) + name = entity.GetComponent().Tag.c_str(); + + const uint32_t maxSearchDepth = 10; + bool hasChildMatchingSearch = TagSearchRecursive(entity, searchFilter, maxSearchDepth); + + if (!UI::IsMatchingSearch(name, searchFilter) && !hasChildMatchingSearch) + return; + + const float edgeOffset = 4.0f; + const float rowHeight = 21.0f; + + // ImGui item height tweaks + auto* window = ImGui::GetCurrentWindow(); + window->DC.CurrLineSize.y = rowHeight; + //--------------------------------------------- + ImGui::TableNextRow(0, rowHeight); + + // Label column + //------------- + + ImGui::TableNextColumn(); + + ImGui::PushID((const void*)(uint64_t)entity.GetUUID()); + + window->DC.CurrLineTextBaseOffset = 3.0f; + + const ImVec2 rowAreaMin = ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).Min; + const ImVec2 rowAreaMax = { ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), ImGui::TableGetColumnCount() - 1).Max.x - 20, + rowAreaMin.y + rowHeight }; + + const bool isSelected = SelectionManager::IsSelected(s_ActiveSelectionContext, entity.GetUUID()); + + ImGuiTreeNodeFlags flags = (isSelected ? ImGuiTreeNodeFlags_Selected : 0) | ImGuiTreeNodeFlags_OpenOnArrow; + flags |= ImGuiTreeNodeFlags_SpanAvailWidth; + + if (hasChildMatchingSearch) + flags |= ImGuiTreeNodeFlags_DefaultOpen; + + if (entity.Children().empty()) + flags |= ImGuiTreeNodeFlags_Leaf; + + + const std::string strID = name; + const std::string strIDButtonBehavior = std::format("{0}BB", name); + + ImGui::PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); + ImGui::PushClipRect(rowAreaMin, rowAreaMax, false); + bool isRowHovered, held; + bool isRowClicked = ImGui::ButtonBehavior(ImRect(rowAreaMin, rowAreaMax), ImGui::GetID(strID.c_str()), + &isRowHovered, &held, ImGuiButtonFlags_AllowOverlap | ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + bool wasRowRightClicked = ImGui::IsMouseReleased(ImGuiMouseButton_Right); + + ImGui::SetItemAllowOverlap(); + + ImGui::PopClipRect(); + ImGui::PopItemFlag(); + + const bool isWindowFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + + const auto& editorSettings = EditorApplicationSettings::Get(); + // Row colouring + //-------------- + + // Fill with light selection colour if any of the child entities selected + auto isAnyDescendantSelected = [&](Entity ent, auto isAnyDescendantSelected) -> bool + { + if (SelectionManager::IsSelected(s_ActiveSelectionContext, ent.GetUUID())) + return true; + + if (!ent.Children().empty()) + { + for (auto& childEntityID : ent.Children()) + { + Entity childEntity = m_Context->GetEntityWithUUID(childEntityID); + if (isAnyDescendantSelected(childEntity, isAnyDescendantSelected)) + return true; + } + } + + return false; + }; + + auto fillRowWithColour = [](const ImColor& colour) + { + for (int column = 0; column < ImGui::TableGetColumnCount(); column++) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, colour, column); + }; + + if (isSelected) + { + if (isWindowFocused || UI::NavigatedTo()) + fillRowWithColour(Colors::Theme::selection); + else + { + const ImColor col = UI::ColourWithMultipliedValue(Colors::Theme::selection, 0.9f); + fillRowWithColour(UI::ColourWithMultipliedSaturation(col, 0.7f)); + } + } + else if (isRowHovered) + { + fillRowWithColour(Colors::Theme::groupHeader); + } + else if (isAnyDescendantSelected(entity, isAnyDescendantSelected)) + { + fillRowWithColour(Colors::Theme::selectionMuted); + } + + // Text colouring + //--------------- + + if (isSelected) + ImGui::PushStyleColor(ImGuiCol_Text, Colors::Theme::backgroundDark); + + bool isPrefab = entity.HasComponent(); + bool isMeshChild = entity.HasComponent(); + bool isValidPrefab = false; + if (isPrefab) + isValidPrefab = AssetManager::IsAssetHandleValid(entity.GetComponent().PrefabID); + + if (isPrefab && !isSelected) + ImGui::PushStyleColor(ImGuiCol_Text, isValidPrefab ? Colors::Theme::validPrefab : Colors::Theme::invalidPrefab); + + if(isMeshChild && !isPrefab && !isSelected) + ImGui::PushStyleColor(ImGuiCol_Text, Colors::Theme::validPrefab); + + bool isMeshValid = true; + if (editorSettings.HighlightUnsetMeshes && !isSelected && !isPrefab) + { + if (entity.HasComponent()) + isMeshValid = AssetManager::IsAssetHandleValid(entity.GetComponent().Mesh); + else if (entity.HasComponent()) + isMeshValid = AssetManager::IsAssetHandleValid(entity.GetComponent().StaticMesh); + + if (!isMeshValid) + ImGui::PushStyleColor(ImGuiCol_Text, Colors::Theme::meshNotSet); + } + + // Tree node + //---------- + // TODO: clean up this mess + ImGuiContext& g = *GImGui; + auto& style = ImGui::GetStyle(); + const ImVec2 label_size = ImGui::CalcTextSize(strID.c_str(), nullptr, false); + const ImVec2 padding = ((flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y)); + const float text_offset_x = g.FontSize + padding.x * 2; // Collapser arrow width + Spacing + const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it + const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapser + ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); + const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x; + const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x; + const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2); + + bool previousState = ImGui::TreeNodeUpdateNextOpen(ImGui::GetID(strID.c_str()), ImGuiTreeNodeFlags_None); + + if (is_mouse_x_over_arrow && isRowClicked) + ImGui::SetNextItemOpen(!previousState); + + if (!isSelected && isAnyDescendantSelected(entity, isAnyDescendantSelected)) + ImGui::SetNextItemOpen(true); + + const bool opened = ImGui::TreeNodeWithIcon(nullptr, ImGui::GetID(strID.c_str()), flags, name, nullptr); + + int32_t rowIndex = ImGui::TableGetRowIndex(); + if (rowIndex >= m_FirstSelectedRow && rowIndex <= m_LastSelectedRow && !SelectionManager::IsSelected(entity.GetUUID()) && m_ShiftSelectionRunning) + { + SelectionManager::Select(s_ActiveSelectionContext, entity.GetUUID()); + + if (SelectionManager::GetSelectionCount(s_ActiveSelectionContext) == (m_LastSelectedRow - m_FirstSelectedRow) + 1) + { + m_ShiftSelectionRunning = false; + } + } + + const std::string rightClickPopupID = std::format("{0}-ContextMenu", strID); + + bool entityDeleted = false; + if (ImGui::BeginPopupContextItem(rightClickPopupID.c_str())) + { + { + UI::ScopedColour colText(ImGuiCol_Text, Colors::Theme::text); + UI::ScopedColourStack entitySelection(ImGuiCol_Header, Colors::Theme::groupHeader, + ImGuiCol_HeaderHovered, Colors::Theme::groupHeader, + ImGuiCol_HeaderActive, Colors::Theme::groupHeader); + + bool isMeshChild = entity.HasComponent(); + if (!isSelected) + { + if (!Input::IsKeyDown(KeyCode::LeftControl)) + SelectionManager::DeselectAll(); + + SelectionManager::Select(s_ActiveSelectionContext, entity.GetUUID()); + } + + { + UI::ScopedDisable disable(!entity.GetParent() || isMeshChild); + if (ImGui::MenuItem("Unparent")) + m_Context->UnparentEntity(entity, true); + } + + if (isPrefab && isValidPrefab) + { + if (ImGui::MenuItem("Update Prefab")) + { + AssetHandle prefabAssetHandle = entity.GetComponent().PrefabID; + Ref prefab = AssetManager::GetAsset(prefabAssetHandle); + if (prefab) + prefab->Create(entity); + else + SE_ERROR("Prefab has invalid asset handle: {0}", prefabAssetHandle); + } + } + + DrawEntityCreateMenu(entity); + + { + UI::ScopedDisable disable(isMeshChild); + if (ImGui::MenuItem("Delete")) + entityDeleted = true; + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Reset Transform to Mesh")) + m_Context->ResetTransformsToMesh(entity, false); + + if (ImGui::MenuItem("Reset All Transforms to Mesh")) + m_Context->ResetTransformsToMesh(entity, true); + + + if (!m_EntityContextMenuPlugins.empty()) + { + ImGui::Separator(); + + if (ImGui::MenuItem("Set Transform to Editor Camera Transform")) + { + for (auto& func : m_EntityContextMenuPlugins) + { + func(entity); + } + } + } + } + + ImGui::EndPopup(); + } + + // Type column + //------------ + if (isRowClicked) + { + if (wasRowRightClicked) + { + ImGui::OpenPopup(rightClickPopupID.c_str()); + } + else + { + bool ctrlDown = Input::IsKeyDown(KeyCode::LeftControl) || Input::IsKeyDown(KeyCode::RightControl); + bool shiftDown = Input::IsKeyDown(KeyCode::LeftShift) || Input::IsKeyDown(KeyCode::RightShift); + if (shiftDown && SelectionManager::GetSelectionCount(s_ActiveSelectionContext) > 0) + { + SelectionManager::DeselectAll(s_ActiveSelectionContext); + + if (rowIndex < m_FirstSelectedRow) + { + m_LastSelectedRow = m_FirstSelectedRow; + m_FirstSelectedRow = rowIndex; + } + else + { + m_LastSelectedRow = rowIndex; + } + + m_ShiftSelectionRunning = true; + } + else if (!ctrlDown || shiftDown) + { + SelectionManager::DeselectAll(); + SelectionManager::Select(s_ActiveSelectionContext, entity.GetUUID()); + m_FirstSelectedRow = rowIndex; + m_LastSelectedRow = -1; + } + else + { + if (isSelected) + SelectionManager::Deselect(s_ActiveSelectionContext, entity.GetUUID()); + else + SelectionManager::Select(s_ActiveSelectionContext, entity.GetUUID()); + } + } + + ImGui::FocusWindow(ImGui::GetCurrentWindow()); + } + + if (isSelected || isPrefab || isMeshChild || (editorSettings.HighlightUnsetMeshes && !isMeshValid)) + ImGui::PopStyleColor(); + + // Drag & Drop + //------------ + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) + { + const auto& selectedEntities = SelectionManager::GetSelections(s_ActiveSelectionContext); + UUID entityID = entity.GetUUID(); + + if (!SelectionManager::IsSelected(s_ActiveSelectionContext, entityID)) + { + const char* name = entity.Name().c_str(); + ImGui::TextUnformatted(name); + ImGui::SetDragDropPayload("scene_entity_hierarchy", &entityID, 1 * sizeof(UUID)); + } + else + { + for (const auto& selectedEntity : selectedEntities) + { + Entity e = m_Context->GetEntityWithUUID(selectedEntity); + const char* name = e.Name().c_str(); + ImGui::TextUnformatted(name); + } + + ImGui::SetDragDropPayload("scene_entity_hierarchy", selectedEntities.data(), selectedEntities.size() * sizeof(UUID)); + } + + ImGui::EndDragDropSource(); + } + + if (ImGui::BeginDragDropTarget()) + { + const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("scene_entity_hierarchy", ImGuiDragDropFlags_AcceptNoDrawDefaultRect); + + if (payload) + { + size_t count = payload->DataSize / sizeof(UUID); + + // Do not allow user to rearrange the hierarchy of a mesh entity + bool canDrop = true; + for (size_t i = 0; i < count; i++) + { + UUID droppedEntityID = *(((UUID*)payload->Data) + i); + Entity droppedEntity = m_Context->GetEntityWithUUID(droppedEntityID); + if (droppedEntity.HasComponent()) canDrop = false; + + if (canDrop && entity.HasComponent()) + { + for(auto parent = entity.GetParent(); parent; parent = parent.GetParent()) + { + if (parent == droppedEntity) + { + canDrop = false; + break; + } + } + } + + if (!canDrop) break; + } + + if (canDrop) + { + for (size_t i = 0; i < count; i++) + { + UUID droppedEntityID = *(((UUID*)payload->Data) + i); + Entity droppedEntity = m_Context->GetEntityWithUUID(droppedEntityID); + m_Context->ParentEntity(droppedEntity, entity); + } + } + } + + ImGui::EndDragDropTarget(); + } + + + ImGui::TableNextColumn(); + if (isPrefab) + { + UI::ShiftCursorX(edgeOffset * 3.0f); + + if (isSelected) + ImGui::PushStyleColor(ImGuiCol_Text, Colors::Theme::backgroundDark); + + ImGui::TextUnformatted("Prefab"); + + if (isSelected) + ImGui::PopStyleColor(); + } + else if (isMeshChild) + { + UI::ShiftCursorX(edgeOffset * 3.0f); + + if (isSelected) + ImGui::PushStyleColor(ImGuiCol_Text, Colors::Theme::backgroundDark); + + ImGui::TextUnformatted("Submesh"); + + if (isSelected) + ImGui::PopStyleColor(); + } + + // Draw children + //-------------- + + if (opened) + { + for (auto child : entity.Children()) + DrawEntityNode(m_Context->GetEntityWithUUID(child), ""); + + ImGui::TreePop(); + } + + // Defer deletion until end of node UI + if (entityDeleted) + { + // NOTE(Peter): Intentional copy since DestroyEntity would call EditorLayer::OnEntityDeleted which deselects the entity + auto selectedEntities = SelectionManager::GetSelections(s_ActiveSelectionContext); + for (auto entityID : selectedEntities) + m_Context->DestroyEntity(m_Context->GetEntityWithUUID(entityID)); + } + + ImGui::PopID(); + } + + static bool DrawVec3Control(const std::string_view label, glm::vec3& value, bool& manuallyEdited, float resetValue = 0.0f, float columnWidth = 100.0f, UI::VectorAxis renderMultiSelectAxes = UI::VectorAxis::None) + { + bool modified = false; + + UI::PushID(); + ImGui::TableSetColumnIndex(0); + UI::ShiftCursor(17.0f, 7.0f); + + ImGui::Text(label.data()); + UI::Draw::Underline(false, 0.0f, 2.0f); + + ImGui::TableSetColumnIndex(1); + UI::ShiftCursor(7.0f, 0.0f); + + modified = UI::Widgets::EditVec3(label, ImVec2(ImGui::GetContentRegionAvail().x - 8.0f, ImGui::GetFrameHeightWithSpacing() + 8.0f), resetValue, manuallyEdited, value, renderMultiSelectAxes); + UI::PopID(); + + return modified; + } + + template + void DrawMaterialTable(SceneHierarchyPanel* _this, const std::vector& entities, Ref meshMaterialTable, Ref localMaterialTable) + { + if (UI::BeginTreeNode("Materials")) + { + UI::BeginPropertyGrid(); + + if (localMaterialTable->GetMaterialCount() != meshMaterialTable->GetMaterialCount()) + localMaterialTable->SetMaterialCount(meshMaterialTable->GetMaterialCount()); + + for (uint32_t i = 0; i < (uint32_t)localMaterialTable->GetMaterialCount(); i++) + { + if (i == meshMaterialTable->GetMaterialCount()) + ImGui::Separator(); + + bool hasLocalMaterial = localMaterialTable->HasMaterial(i); + bool hasMeshMaterial = meshMaterialTable->HasMaterial(i); + + std::string label = std::format("[Material {0}]", i); + + // NOTE(Peter): Fix for weird ImGui ID bug... + std::string id = std::format("{0}-{1}", label, i); + ImGui::PushID(id.c_str()); + + UI::PropertyAssetReferenceSettings settings; + if (hasMeshMaterial && !hasLocalMaterial) + { + AssetHandle meshMaterialAssetHandle = meshMaterialTable->GetMaterial(i); + Ref meshMaterialAsset = AssetManager::GetAsset(meshMaterialAssetHandle); + std::string meshMaterialName = meshMaterialAsset->GetMaterial()->GetName(); + if (meshMaterialName.empty()) + meshMaterialName = "Unnamed Material"; + + AssetHandle materialAssetHandle = meshMaterialAsset->Handle; + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, entities.size() > 1 && _this->IsInconsistentPrimitive([i](const TComponent& component) + { + Ref materialTable = nullptr; + if constexpr (std::is_same_v) + materialTable = AssetManager::GetAsset(component.Mesh)->GetMaterials(); + else + materialTable = AssetManager::GetAsset(component.StaticMesh)->GetMaterials(); + + if (!materialTable || i >= materialTable->GetMaterialCount()) + return (AssetHandle)0; + + return materialTable->GetMaterial(i); + })); + + UI::PropertyAssetReferenceTarget(label.c_str(), meshMaterialName.c_str(), materialAssetHandle, [_this, &entities, i, localMaterialTable](Ref materialAsset) mutable + { + Ref context = _this->GetSceneContext(); + + for (auto entityID : entities) + { + Entity entity = context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (materialAsset == UUID(0)) + component.MaterialTable->ClearMaterial(i); + else + component.MaterialTable->SetMaterial(i, materialAsset->Handle); + } + }, "", settings); + + ImGui::PopItemFlag(); + } + else + { + // hasMeshMaterial is false, hasLocalMaterial could be true or false + AssetHandle materialAssetHandle = 0; + if (hasLocalMaterial) + { + materialAssetHandle = localMaterialTable->GetMaterial(i); + settings.AdvanceToNextColumn = false; + settings.WidthOffset = ImGui::GetStyle().ItemSpacing.x + 28.0f; + } + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, entities.size() > 1 && _this->IsInconsistentPrimitive([i, localMaterialTable](const TComponent& component) + { + Ref materialTable = component.MaterialTable; + + if (!materialTable || i >= materialTable->GetMaterialCount()) + return (AssetHandle)0; + + if (!materialTable->HasMaterial(i)) + return (AssetHandle)0; + + return materialTable->GetMaterial(i); + })); + + UI::PropertyAssetReferenceTarget(label.c_str(), nullptr, materialAssetHandle, [_this, &entities, i, localMaterialTable](Ref materialAsset) mutable + { + Ref context = _this->GetSceneContext(); + + for (auto entityID : entities) + { + Entity entity = context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (materialAsset == UUID(0)) + component.MaterialTable->ClearMaterial(i); + else + component.MaterialTable->SetMaterial(i, materialAsset->Handle); + } + }, "", settings); + + ImGui::PopItemFlag(); + } + + if (hasLocalMaterial) + { + ImGui::SameLine(); + float prevItemHeight = ImGui::GetItemRectSize().y; + if (ImGui::Button(UI::GenerateLabelID("X"), { prevItemHeight, prevItemHeight })) + { + Ref context = _this->GetSceneContext(); + + for (auto entityID : entities) + { + Entity entity = context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + component.MaterialTable->ClearMaterial(i); + } + } + ImGui::NextColumn(); + } + + ImGui::PopID(); + } + + UI::EndPropertyGrid(); + UI::EndTreeNode(); + } + } + + template + void DrawMaterialTableFunc(SceneHierarchyPanel* _this, const std::vector& entities, Ref meshMaterialTable, Ref localMaterialTable, const std::function(const TComponent&)>& componentMaterialTable) + { + if (UI::BeginTreeNode("Materials")) + { + UI::BeginPropertyGrid(); + + if (localMaterialTable->GetMaterialCount() != meshMaterialTable->GetMaterialCount()) + localMaterialTable->SetMaterialCount(meshMaterialTable->GetMaterialCount()); + + for (uint32_t i = 0; i < (uint32_t)localMaterialTable->GetMaterialCount(); i++) + { + if (i == meshMaterialTable->GetMaterialCount()) + ImGui::Separator(); + + bool hasLocalMaterial = localMaterialTable->HasMaterial(i); + bool hasMeshMaterial = meshMaterialTable->HasMaterial(i); + + std::string label = std::format("[Material {0}]", i); + + // NOTE(Peter): Fix for weird ImGui ID bug... + std::string id = std::format("{0}-{1}", label, i); + ImGui::PushID(id.c_str()); + + UI::PropertyAssetReferenceSettings settings; + if (hasMeshMaterial && !hasLocalMaterial) + { + AssetHandle meshMaterialAssetHandle = meshMaterialTable->GetMaterial(i); + Ref meshMaterialAsset = AssetManager::GetAsset(meshMaterialAssetHandle); + std::string meshMaterialName = meshMaterialAsset->GetMaterial()->GetName(); + if (meshMaterialName.empty()) + meshMaterialName = "Unnamed Material"; + + AssetHandle materialAssetHandle = meshMaterialAsset->Handle; + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, entities.size() > 1 && _this->IsInconsistentPrimitive([i](const TComponent& component) + { + Ref materialTable = nullptr; + if constexpr (std::is_same_v) + materialTable = AssetManager::GetAsset(component.Mesh)->GetMaterials(); + else + materialTable = AssetManager::GetAsset(component.StaticMesh)->GetMaterials(); + + if (!materialTable || i >= materialTable->GetMaterialCount()) + return (AssetHandle)0; + + return materialTable->GetMaterial(i); + })); + + UI::PropertyAssetReferenceTarget(label.c_str(), meshMaterialName.c_str(), materialAssetHandle, [_this, &entities, i, localMaterialTable, &componentMaterialTable](Ref materialAsset) mutable + { + Ref context = _this->GetSceneContext(); + + for (auto entityID : entities) + { + Entity entity = context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + auto materialTable = componentMaterialTable(component); + + if (materialAsset == UUID(0)) + materialTable->ClearMaterial(i); + else + materialTable->SetMaterial(i, materialAsset->Handle); + } + }, "", settings); + + ImGui::PopItemFlag(); + } + else + { + // hasMeshMaterial is false, hasLocalMaterial could be true or false + AssetHandle materialAssetHandle = 0; + if (hasLocalMaterial) + { + materialAssetHandle = localMaterialTable->GetMaterial(i); + settings.AdvanceToNextColumn = false; + settings.WidthOffset = ImGui::GetStyle().ItemSpacing.x + 28.0f; + } + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, entities.size() > 1 && _this->IsInconsistentPrimitive([i, localMaterialTable, &componentMaterialTable](const TComponent& component) + { + Ref materialTable = componentMaterialTable(component); + + if (!materialTable || i >= materialTable->GetMaterialCount()) + return (AssetHandle)0; + + if (!materialTable->HasMaterial(i)) + return (AssetHandle)0; + + return materialTable->GetMaterial(i); + })); + + UI::PropertyAssetReferenceTarget(label.c_str(), nullptr, materialAssetHandle, [_this, &entities, i, localMaterialTable, &componentMaterialTable](Ref materialAsset) mutable + { + Ref context = _this->GetSceneContext(); + + for (auto entityID : entities) + { + Entity entity = context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + auto materialTable = componentMaterialTable(component); + + if (materialAsset == UUID(0)) + materialTable->ClearMaterial(i); + else + materialTable->SetMaterial(i, materialAsset->Handle); + } + }, "", settings); + + ImGui::PopItemFlag(); + } + + if (hasLocalMaterial) + { + ImGui::SameLine(); + float prevItemHeight = ImGui::GetItemRectSize().y; + if (ImGui::Button(UI::GenerateLabelID("X"), { prevItemHeight, prevItemHeight })) + { + Ref context = _this->GetSceneContext(); + + for (auto entityID : entities) + { + Entity entity = context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + auto materialTable = componentMaterialTable(component); + materialTable->ClearMaterial(i); + } + } + ImGui::NextColumn(); + } + + ImGui::PopID(); + } + + UI::EndPropertyGrid(); + UI::EndTreeNode(); + } + } + + template + void DrawSimpleAddComponentButton(SceneHierarchyPanel* _this, const std::string& name, Ref icon = nullptr) + { + bool canAddComponent = false; + + for (const auto& entityID : SelectionManager::GetSelections(SceneHierarchyPanel::GetActiveSelectionContext())) + { + Entity entity = _this->GetSceneContext()->GetEntityWithUUID(entityID); + if (!entity.HasComponent()) + { + canAddComponent = true; + break; + } + } + + if (!canAddComponent) + return; + + if (icon == nullptr) + icon = EditorResources::AssetIcon; + + const float rowHeight = 25.0f; + auto* window = ImGui::GetCurrentWindow(); + window->DC.CurrLineSize.y = rowHeight; + ImGui::TableNextRow(0, rowHeight); + ImGui::TableSetColumnIndex(0); + + window->DC.CurrLineTextBaseOffset = 3.0f; + + const ImVec2 rowAreaMin = ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).Min; + const ImVec2 rowAreaMax = { ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), ImGui::TableGetColumnCount() - 1).Max.x - 20, + rowAreaMin.y + rowHeight }; + + ImGui::PushClipRect(rowAreaMin, rowAreaMax, false); + bool isRowHovered, held; + bool isRowClicked = ImGui::ButtonBehavior(ImRect(rowAreaMin, rowAreaMax), ImGui::GetID(name.c_str()), &isRowHovered, &held, ImGuiButtonFlags_AllowOverlap); + ImGui::SetItemAllowOverlap(); + ImGui::PopClipRect(); + + auto fillRowWithColour = [](const ImColor& colour) + { + for (int column = 0; column < ImGui::TableGetColumnCount(); column++) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, colour, column); + }; + + if (isRowHovered) + fillRowWithColour(Colors::Theme::background); + + UI::ShiftCursor(1.5f, 1.5f); + UI::Image(icon, { rowHeight - 3.0f, rowHeight - 3.0f }); + UI::ShiftCursor(-1.5f, -1.5f); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + ImGui::TextUnformatted(name.c_str()); + + if (isRowClicked) + { + for (const auto& entityID : SelectionManager::GetSelections(SceneHierarchyPanel::GetActiveSelectionContext())) + { + Entity entity = _this->GetSceneContext()->GetEntityWithUUID(entityID); + + if (sizeof...(TIncompatibleComponents) > 0 && entity.HasComponent()) + continue; + + if (!entity.HasComponent()) + entity.AddComponent(); + } + + ImGui::CloseCurrentPopup(); + } + } + + template + void DrawAddComponentButton(SceneHierarchyPanel* _this, const std::string& name, OnAddedFunction onComponentAdded, Ref icon = nullptr) + { + bool canAddComponent = false; + + for (const auto& entityID : SelectionManager::GetSelections(SceneHierarchyPanel::GetActiveSelectionContext())) + { + Entity entity = _this->GetSceneContext()->GetEntityWithUUID(entityID); + if (!entity.HasComponent()) + { + canAddComponent = true; + break; + } + } + + if (!canAddComponent) + return; + + if (icon == nullptr) + icon = EditorResources::AssetIcon; + + const float rowHeight = 25.0f; + auto* window = ImGui::GetCurrentWindow(); + window->DC.CurrLineSize.y = rowHeight; + ImGui::TableNextRow(0, rowHeight); + ImGui::TableSetColumnIndex(0); + + window->DC.CurrLineTextBaseOffset = 3.0f; + + const ImVec2 rowAreaMin = ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).Min; + const ImVec2 rowAreaMax = { ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), ImGui::TableGetColumnCount() - 1).Max.x - 20, + rowAreaMin.y + rowHeight }; + + ImGui::PushClipRect(rowAreaMin, rowAreaMax, false); + bool isRowHovered, held; + bool isRowClicked = ImGui::ButtonBehavior(ImRect(rowAreaMin, rowAreaMax), ImGui::GetID(name.c_str()), &isRowHovered, &held, ImGuiButtonFlags_AllowOverlap); + ImGui::SetItemAllowOverlap(); + ImGui::PopClipRect(); + + auto fillRowWithColour = [](const ImColor& colour) + { + for (int column = 0; column < ImGui::TableGetColumnCount(); column++) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, colour, column); + }; + + if (isRowHovered) + fillRowWithColour(Colors::Theme::background); + + UI::ShiftCursor(1.5f, 1.5f); + UI::Image(icon, { rowHeight - 3.0f, rowHeight - 3.0f }); + UI::ShiftCursor(-1.5f, -1.5f); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + ImGui::TextUnformatted(name.c_str()); + + if (isRowClicked) + { + for (const auto& entityID : SelectionManager::GetSelections(SceneHierarchyPanel::GetActiveSelectionContext())) + { + Entity entity = _this->GetSceneContext()->GetEntityWithUUID(entityID); + + if (sizeof...(TIncompatibleComponents) > 0 && entity.HasComponent()) + continue; + + if (!entity.HasComponent()) + { + auto& component = entity.AddComponent(); + onComponentAdded(entity, component); + } + } + + ImGui::CloseCurrentPopup(); + } + } + + void SceneHierarchyPanel::DrawComponents(const std::vector& entityIDs) + { + if (entityIDs.size() == 0) + return; + + ImGui::AlignTextToFramePadding(); + + ImVec2 contentRegionAvailable = ImGui::GetContentRegionAvail(); + + UI::ShiftCursor(4.0f, 4.0f); + + bool isHoveringID = false; + const bool isMultiSelect = entityIDs.size() > 1; + + Entity firstEntity = m_Context->GetEntityWithUUID(entityIDs[0]); + + // Draw Tag Field + { + const float iconOffset = 6.0f; + UI::ShiftCursor(4.0f, iconOffset); + UI::Image(EditorResources::PencilIcon, ImVec2((float)EditorResources::PencilIcon->GetWidth(), (float)EditorResources::PencilIcon->GetHeight()), + ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + ImColor(128, 128, 128, 255).Value); + + ImGui::SameLine(0.0f, 4.0f); + UI::ShiftCursorY(-iconOffset); + + const bool inconsistentTag = IsInconsistentString([](const TagComponent& tagComponent) { return tagComponent.Tag; }); + const std::string& tag = (isMultiSelect && inconsistentTag) ? "---" : firstEntity.Name(); + + char buffer[256]; + memset(buffer, 0, 256); + buffer[0] = 0; // Setting the first byte to 0 makes checking if string is empty easier later. + memcpy(buffer, tag.c_str(), tag.length()); + ImGui::PushItemWidth(contentRegionAvailable.x * 0.5f); + UI::ScopedStyle frameBorder(ImGuiStyleVar_FrameBorderSize, 0.0f); + UI::ScopedColour frameColour(ImGuiCol_FrameBg, IM_COL32(0, 0, 0, 0)); + UI::ScopedFont boldFont(ImGui::GetIO().Fonts->Fonts[0]); + + if (Input::IsKeyDown(KeyCode::F2) && (m_IsHierarchyOrPropertiesFocused || UI::IsWindowFocused("Viewport")) && !ImGui::IsAnyItemActive()) + ImGui::SetKeyboardFocusHere(); + + if (ImGui::InputText("##Tag", buffer, 256)) + { + for (auto entityID : entityIDs) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + if(buffer[0] == 0) + memcpy(buffer, "Unnamed Entity", 16); // if the entity has no name, the name will be set to Unnamed Entity, this prevents invisible entities in SHP. + + entity.GetComponent().Tag = buffer; + } + } + + UI::DrawItemActivityOutline(UI::OutlineFlags_NoOutlineInactive); + + isHoveringID = ImGui::IsItemHovered(); + + ImGui::PopItemWidth(); + } + + ImGui::SameLine(); + UI::ShiftCursorX(-5.0f); + + float lineHeight = GImGui->Font->FontSize + GImGui->Style.FramePadding.y * 2.0f; + + ImVec2 addTextSize = ImGui::CalcTextSize(" ADD "); + addTextSize.x += GImGui->Style.FramePadding.x * 2.0f; + + { + UI::ScopedColourStack addCompButtonColours(ImGuiCol_Button, IM_COL32(70, 70, 70, 200), + ImGuiCol_ButtonHovered, IM_COL32(70, 70, 70, 255), + ImGuiCol_ButtonActive, IM_COL32(70, 70, 70, 150)); + + ImGui::SameLine(contentRegionAvailable.x - (addTextSize.x + GImGui->Style.FramePadding.x)); + if (ImGui::Button(" ADD ", ImVec2(addTextSize.x + 4.0f, lineHeight + 2.0f))) + ImGui::OpenPopup("AddComponentPanel"); + + const float pad = 4.0f; + const float iconWidth = ImGui::GetFrameHeight() - pad * 2.0f; + const float iconHeight = iconWidth; + ImVec2 iconPos = ImGui::GetItemRectMax(); + iconPos.x -= iconWidth + pad; + iconPos.y -= iconHeight + pad; + ImGui::SetCursorScreenPos(iconPos); + UI::ShiftCursor(-5.0f, -1.0f); + + UI::Image(EditorResources::PlusIcon, ImVec2(iconWidth, iconHeight), + ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), + ImColor(160, 160, 160, 255).Value); + } + + float addComponentPanelStartY = ImGui::GetCursorScreenPos().y; + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); + + { + UI::ScopedFont boldFont(ImGui::GetIO().Fonts->Fonts[0]); + UI::ScopedStyle itemSpacing(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + UI::ScopedStyle windowPadding(ImGuiStyleVar_WindowPadding, ImVec2(5, 10)); + UI::ScopedStyle windowRounding(ImGuiStyleVar_PopupRounding, 4.0f); + UI::ScopedStyle cellPadding(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0)); + + static float addComponentPanelWidth = 250.0f; + ImVec2 windowPos = ImGui::GetWindowPos(); + const float maxHeight = ImGui::GetContentRegionMax().y - 60.0f; + + ImGui::SetNextWindowPos({ windowPos.x + addComponentPanelWidth / 1.3f, addComponentPanelStartY}); + ImGui::SetNextWindowSizeConstraints({ 50.0f, 50.0f }, { addComponentPanelWidth, maxHeight }); + if (ImGui::BeginPopup("AddComponentPanel", ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking)) + { + // Setup Table + if (ImGui::BeginTable("##component_table", 2, ImGuiTableFlags_SizingStretchSame)) + { + ImGui::TableSetupColumn("Icon", ImGuiTableColumnFlags_WidthFixed, addComponentPanelWidth * 0.15f); + ImGui::TableSetupColumn("ComponentNames", ImGuiTableColumnFlags_WidthFixed, addComponentPanelWidth * 0.85f); + + DrawSimpleAddComponentButton(this, "Camera", EditorResources::CameraIcon); + DrawSimpleAddComponentButton(this, "Mesh", EditorResources::MeshIcon); + DrawSimpleAddComponentButton(this, "Static Mesh", EditorResources::StaticMeshIcon); + DrawSimpleAddComponentButton(this, "Directional Light", EditorResources::DirectionalLightIcon); + DrawSimpleAddComponentButton(this, "Point Light", EditorResources::PointLightIcon); + DrawSimpleAddComponentButton(this, "Spot Light", EditorResources::SpotLightIcon); + DrawSimpleAddComponentButton(this, "Sky Light", EditorResources::SkyLightIcon); + DrawSimpleAddComponentButton(this, "Script", EditorResources::ScriptIcon); + DrawSimpleAddComponentButton(this, "Sprite Renderer", EditorResources::SpriteIcon); + //DrawSimpleAddComponentButton(this, "Animation", EditorResources::AnimationIcon); + DrawAddComponentButton(this, "Text", [](Entity entity, TextComponent& tc) + { + tc.FontHandle = Font::GetDefaultFont()->Handle; + }, EditorResources::TextIcon); + DrawSimpleAddComponentButton(this, "Rigidbody 2D", EditorResources::RigidBody2DIcon); + DrawSimpleAddComponentButton(this, "Box Collider 2D", EditorResources::BoxCollider2DIcon); + DrawSimpleAddComponentButton(this, "Circle Collider 2D", EditorResources::CircleCollider2DIcon); + DrawSimpleAddComponentButton(this, "Rigidbody", EditorResources::RigidBodyIcon); + DrawSimpleAddComponentButton(this, "Character Controller", EditorResources::CharacterControllerIcon); + DrawAddComponentButton(this, "Compound Collider", [&](Entity entity, CompoundColliderComponent& component) + { + component.CompoundedColliderEntities = m_Context->GetAllChildren(entity); + component.CompoundedColliderEntities.push_back(entity.GetUUID()); + }, EditorResources::CompoundColliderIcon); + DrawSimpleAddComponentButton(this, "Box Collider", EditorResources::BoxColliderIcon); + DrawSimpleAddComponentButton(this, "Sphere Collider", EditorResources::SphereColliderIcon); + DrawSimpleAddComponentButton(this, "Capsule Collider", EditorResources::CapsuleColliderIcon); + DrawAddComponentButton(this, "Mesh Collider", [&](Entity entity, MeshColliderComponent& colliderComponent) + { + PhysicsSystem::GetOrCreateColliderAsset(entity, colliderComponent); + }, EditorResources::MeshColliderIcon); + /*DrawSimpleAddComponentButton(this, "Audio", EditorResources::AudioIcon); + DrawAddComponentButton(this, "Audio Listener", [&](Entity entity, AudioListenerComponent& alc) + { + auto view = m_Context->GetAllEntitiesWith(); + alc.Active = view.size() == 1; + MiniAudioEngine::Get().RegisterNewListener(alc); + }, EditorResources::AudioListenerIcon);*/ + DrawSimpleAddComponentButton(this, "Tile Renderer", EditorResources::StaticMeshIcon); + + ImGui::EndTable(); + } + + ImGui::EndPopup(); + } + } + + const auto& editorSettings = EditorApplicationSettings::Get(); + if (editorSettings.AdvancedMode) + { + DrawComponent("Prefab", [&](PrefabComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const PrefabComponent& other) { return other.PrefabID; })); + if (UI::PropertyInput("Prefab ID", (uint64_t&)firstComponent.PrefabID)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().PrefabID = firstComponent.PrefabID; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const PrefabComponent& other) { return other.EntityID; })); + if (UI::PropertyInput("Entity ID", (uint64_t&)firstComponent.EntityID)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().EntityID = firstComponent.EntityID; + } + } + + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }); + } + + DrawComponent("Transform", [&](TransformComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::ScopedStyle spacing(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 8.0f)); + UI::ScopedStyle padding(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 4.0f)); + + ImGui::BeginTable("transformComponent", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_NoClip); + ImGui::TableSetupColumn("label_column", 0, 100.0f); + ImGui::TableSetupColumn("value_column", ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_NoClip, ImGui::GetContentRegionAvail().x - 100.0f); + + bool translationManuallyEdited = false; + bool rotationManuallyEdited = false; + bool scaleManuallyEdited = false; + + if (isMultiEdit) + { + UI::VectorAxis translationAxes = GetInconsistentVectorAxis([](const TransformComponent& other) { return other.Translation; }); + UI::VectorAxis rotationAxes = GetInconsistentVectorAxis([](const TransformComponent& other) { return other.GetRotationEuler(); }); + UI::VectorAxis scaleAxes = GetInconsistentVectorAxis([](const TransformComponent& other) { return other.Scale; }); + + glm::vec3 translation = firstComponent.Translation; + glm::vec3 rotation = glm::degrees(firstComponent.GetRotationEuler()); + glm::vec3 scale = firstComponent.Scale; + + glm::vec3 oldTranslation = translation; + glm::vec3 oldRotation = rotation; + glm::vec3 oldScale = scale; + + ImGui::TableNextRow(); + bool changed = DrawVec3Control("Translation", translation, translationManuallyEdited, 0.0f, 100.0f, translationAxes); + + ImGui::TableNextRow(); + changed |= DrawVec3Control("Rotation", rotation, rotationManuallyEdited, 0.0f, 100.0f, rotationAxes); + + ImGui::TableNextRow(); + changed |= DrawVec3Control("Scale", scale, scaleManuallyEdited, 1.0f, 100.0f, scaleAxes); + + if (changed) + { + if (translationManuallyEdited || rotationManuallyEdited || scaleManuallyEdited) + { + translationAxes = GetInconsistentVectorAxis(translation, oldTranslation); + rotationAxes = GetInconsistentVectorAxis(rotation, oldRotation); + scaleAxes = GetInconsistentVectorAxis(scale, oldScale); + + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if ((translationAxes & UI::VectorAxis::X) != UI::VectorAxis::None) + component.Translation.x = translation.x; + if ((translationAxes & UI::VectorAxis::Y) != UI::VectorAxis::None) + component.Translation.y = translation.y; + if ((translationAxes & UI::VectorAxis::Z) != UI::VectorAxis::None) + component.Translation.z = translation.z; + + glm::quat oldRotation = component.GetRotation(); + glm::vec3 newRotationEuler = component.GetRotationEuler(); + if ((rotationAxes & UI::VectorAxis::X) != UI::VectorAxis::None) + newRotationEuler.x = glm::radians(rotation.x); + if ((rotationAxes & UI::VectorAxis::Y) != UI::VectorAxis::None) + newRotationEuler.y = glm::radians(rotation.y); + if ((rotationAxes & UI::VectorAxis::Z) != UI::VectorAxis::None) + newRotationEuler.z = glm::radians(rotation.z); + + component.SetRotationEuler(newRotationEuler); + Utils::ReorientAnimation(entity, oldRotation, component.GetRotation()); + + if ((scaleAxes & UI::VectorAxis::X) != UI::VectorAxis::None) + component.Scale.x = scale.x; + if ((scaleAxes & UI::VectorAxis::Y) != UI::VectorAxis::None) + component.Scale.y = scale.y; + if ((scaleAxes & UI::VectorAxis::Z) != UI::VectorAxis::None) + component.Scale.z = scale.z; + } + } + else + { + glm::vec3 translationDiff = translation - oldTranslation; + glm::vec3 rotationDiff = rotation - oldRotation; + glm::vec3 scaleDiff = scale - oldScale; + + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + component.Translation += translationDiff; + glm::vec3 componentRotation = component.GetRotationEuler(); + componentRotation += glm::radians(rotationDiff); + component.SetRotationEuler(componentRotation); + component.Scale += scaleDiff; + } + } + } + } + else + { + Entity entity = m_Context->GetEntityWithUUID(entities[0]); + auto& component = entity.GetComponent(); + + ImGui::TableNextRow(); + DrawVec3Control("Translation", component.Translation, translationManuallyEdited); + + ImGui::TableNextRow(); + glm::vec3 rotation = glm::degrees(component.GetRotationEuler()); + if (DrawVec3Control("Rotation", rotation, rotationManuallyEdited)) + { + glm::quat oldRotation = component.GetRotation(); + component.SetRotationEuler(glm::radians(rotation)); + Utils::ReorientAnimation(entity, oldRotation, component.GetRotation()); + } + + ImGui::TableNextRow(); + DrawVec3Control("Scale", component.Scale, scaleManuallyEdited, 1.0f); + } + + ImGui::EndTable(); + + UI::ShiftCursorY(-8.0f); + UI::Draw::Underline(); + + UI::ShiftCursorY(18.0f); + }, EditorResources::TransformIcon); + + DrawComponent("Mesh", [&](MeshComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + AssetHandle meshHandle = firstComponent.Mesh; + auto mesh = AssetManager::GetAsset(meshHandle); + auto meshSource = mesh ? AssetManager::GetAsset(mesh->GetMeshSource()) : nullptr; + UI::BeginPropertyGrid(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const MeshComponent& other) { return other.Mesh; })); + UI::PropertyAssetReferenceError error; + if (UI::PropertyAssetReferenceWithConversion("Mesh", meshHandle, + [=](Ref meshAsset) + { + if (m_MeshAssetConvertCallback && !isMultiEdit) + m_MeshAssetConvertCallback(m_Context->GetEntityWithUUID(entities[0]), meshAsset); + },"", & error)) + { + mesh = AssetManager::GetAsset(meshHandle); + meshSource = mesh ? AssetManager::GetAsset(mesh->GetMeshSource()) : nullptr; + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& mc = entity.GetComponent(); + mc.Mesh = meshHandle; + m_Context->RebuildMeshEntityHierarchy(entity); + + //// TODO(Yan): maybe prompt for this, this isn't always expected behaviour + //if (entity.HasComponent()) + //{ + // //CookingFactory::CookMesh(mcc.CollisionMesh); + //} + } + } + ImGui::PopItemFlag(); + + if (error == UI::PropertyAssetReferenceError::InvalidMetadata) + { + if (m_InvalidMetadataCallback && !isMultiEdit) + m_InvalidMetadataCallback(m_Context->GetEntityWithUUID(entities[0]), UI::s_PropertyAssetReferenceAssetHandle); + } + UI::EndPropertyGrid(); + }, EditorResources::MeshIcon); + + DrawComponent("Sub Mesh", [&](SubmeshComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + AssetHandle meshHandle = firstComponent.Mesh; + auto mesh = AssetManager::GetAsset(meshHandle); + auto meshSource = mesh ? AssetManager::GetAsset(mesh->GetMeshSource()) : nullptr; + UI::BeginPropertyGrid(); + if (meshSource) + { + { + UI::ScopedItemFlags itemFlags(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SubmeshComponent& other) { return other.Visible; })); + if (UI::Property("Visible", firstComponent.Visible)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& mc = entity.GetComponent(); + mc.Visible = firstComponent.Visible; + } + } + } + { + uint32_t submeshIndex = firstComponent.SubmeshIndex; + UI::ScopedItemFlags itemFlags(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SubmeshComponent& other) { return other.SubmeshIndex; })); + UI::ScopedDisable disable; + UI::Property("Submesh Index", submeshIndex, 0, (uint32_t)meshSource->GetSubmeshes().size() - 1); + } + } + UI::EndPropertyGrid(); + + if (mesh) + DrawMaterialTable(this, entities, mesh->GetMaterials(), firstComponent.MaterialTable); + }, EditorResources::MeshIcon); + + DrawComponent("Static Mesh", [&](StaticMeshComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + AssetHandle meshHandle = firstComponent.StaticMesh; + auto mesh = AssetManager::GetAsset(firstComponent.StaticMesh); + + UI::BeginPropertyGrid(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const StaticMeshComponent& other) { return other.Visible; })); + if (UI::Property("Visible", firstComponent.Visible)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& mc = entity.GetComponent(); + mc.Visible = firstComponent.Visible; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const StaticMeshComponent& other) { return other.StaticMesh; })); + UI::PropertyAssetReferenceError error; + if (UI::PropertyAssetReferenceWithConversion("Static Mesh", meshHandle, + [=](Ref meshAsset) + { + if (m_MeshAssetConvertCallback && !isMultiEdit) + m_MeshAssetConvertCallback(m_Context->GetEntityWithUUID(entities[0]), meshAsset); + }, "", & error)) + { + mesh = AssetManager::GetAsset(meshHandle); + + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& mc = entity.GetComponent(); + mc.StaticMesh = meshHandle; + + // TODO(Yan): maybe prompt for this, this isn't always expected behaviour + /*if (entity.HasComponent() && mesh) + { + auto& mcc = entity.GetComponent(); + mcc.CollisionMesh = mc.StaticMesh; + CookingFactory::CookMesh(mcc.CollisionMesh); + }*/ + } + } + ImGui::PopItemFlag(); + + if (error == UI::PropertyAssetReferenceError::InvalidMetadata) + { + if (m_InvalidMetadataCallback && !isMultiEdit) + m_InvalidMetadataCallback(m_Context->GetEntityWithUUID(entities[0]), UI::s_PropertyAssetReferenceAssetHandle); + } + + UI::EndPropertyGrid(); + + if (mesh) + DrawMaterialTable(this, entities, mesh->GetMaterials(), firstComponent.MaterialTable); + }, EditorResources::StaticMeshIcon); + + /*DrawComponent("Animation", [&](AnimationComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AnimationComponent& other) { return other.AnimationGraphHandle; })); + + UI::PropertyAssetReferenceError error; + + // draw animation graph reference in error color if the animation controller does not look relevant to this entity + // (e.g. this entity has no bone entities that match the skeleton that is animated by the animation controller) + UI::PropertyAssetReferenceSettings settings; + if (firstComponent.BoneEntityIds.size() == 0) + settings.ButtonLabelColor = ImGui::ColorConvertU32ToFloat4(Colors::Theme::textError); + + if (UI::PropertyAssetReference("Animation Graph", firstComponent.AnimationGraphHandle, "", &error, settings)) + { + auto animationGraphAsset = AssetManager::GetAsset(firstComponent.AnimationGraphHandle); + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& anim = entity.GetComponent(); + anim.AnimationGraphHandle = firstComponent.AnimationGraphHandle; + if (animationGraphAsset) + { + anim.AnimationGraph = animationGraphAsset->CreateInstance(); + if (anim.AnimationGraph) + { + anim.BoneEntityIds = m_Context->FindBoneEntityIds(entity, entity, anim.AnimationGraph->GetSkeleton()); + } + } + else + { + anim.AnimationGraph = nullptr; + anim.BoneEntityIds.clear(); + } + anim.RootBoneTransform = m_Context->FindRootBoneTransform(entity, anim.BoneEntityIds); + } + } + + ImGui::PopItemFlag(); + + if (error == UI::PropertyAssetReferenceError::InvalidMetadata) // TODO: error only reports invalid assets. What about other errors, like: rig not matching animation skeleton? + { + if (m_InvalidMetadataCallback && !isMultiEdit) + m_InvalidMetadataCallback(m_Context->GetEntityWithUUID(entities[0]), UI::s_PropertyAssetReferenceAssetHandle); + } + if (firstComponent.AnimationGraph) + { + for (auto [id, value] : firstComponent.AnimationGraph->Ins) + { + if (value.isFloat32()) EditGraphInput(id, value, entities); + else if (value.isInt32()) EditGraphInput(id, value, entities); + else if (value.isBool()) EditGraphInput(id, value, entities); + else if (value.isVoid()) EditGraphInput(id, value, entities); + else if (value.isObjectWithClassName(type::type_name())) EditGraphInput(id, value, entities); + else if (value.isInt64()) EditGraphInput(id, value, entities); + else if (value.isFloat64()) EditGraphInput(id, value, entities); + } + } + + UI::EndPropertyGrid(); + }, EditorResources::AnimationIcon); + */ + DrawComponent("Camera", [&](CameraComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + // Projection Type + const char* projTypeStrings[] = { "Perspective", "Orthographic" }; + int currentProj = (int)firstComponent.Camera.GetProjectionType(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return (int)other.Camera.GetProjectionType(); })); + if (UI::PropertyDropdown("Projection", projTypeStrings, 2, ¤tProj)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Camera.SetProjectionType((SceneCamera::ProjectionType)currentProj); + } + } + ImGui::PopItemFlag(); + + // Perspective parameters + if (firstComponent.Camera.GetProjectionType() == SceneCamera::ProjectionType::Perspective) + { + float verticalFOV = firstComponent.Camera.GetDegPerspectiveVerticalFOV(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return other.Camera.GetDegPerspectiveVerticalFOV(); })); + if (UI::Property("Vertical FOV", verticalFOV)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Camera.SetDegPerspectiveVerticalFOV(verticalFOV); + } + } + ImGui::PopItemFlag(); + + float nearClip = firstComponent.Camera.GetPerspectiveNearClip(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return other.Camera.GetPerspectiveNearClip(); })); + if (UI::Property("Near Clip", nearClip)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Camera.SetPerspectiveNearClip(nearClip); + } + } + ImGui::PopItemFlag(); + + float farClip = firstComponent.Camera.GetPerspectiveFarClip(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return other.Camera.GetPerspectiveFarClip(); })); + if (UI::Property("Far Clip", farClip)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Camera.SetPerspectiveFarClip(farClip); + } + } + ImGui::PopItemFlag(); + } + + // Orthographic parameters + else if (firstComponent.Camera.GetProjectionType() == SceneCamera::ProjectionType::Orthographic) + { + float orthoSize = firstComponent.Camera.GetOrthographicSize(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return other.Camera.GetOrthographicSize(); })); + if (UI::Property("Size", orthoSize)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Camera.SetOrthographicSize(orthoSize); + } + } + ImGui::PopItemFlag(); + + float nearClip = firstComponent.Camera.GetOrthographicNearClip(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return other.Camera.GetOrthographicNearClip(); })); + if (UI::Property("Near Clip", nearClip)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Camera.SetOrthographicNearClip(nearClip); + } + } + ImGui::PopItemFlag(); + + float farClip = firstComponent.Camera.GetOrthographicFarClip(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return other.Camera.GetOrthographicFarClip(); })); + if (UI::Property("Far Clip", farClip)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Camera.SetOrthographicFarClip(farClip); + } + } + ImGui::PopItemFlag(); + } + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CameraComponent& other) { return other.Primary; })); + if (UI::Property("Main Camera", firstComponent.Primary)) + { + // Does this even make sense??? + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Primary = firstComponent.Primary; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::CameraIcon); + + DrawComponent("Text", [&](TextComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + const bool inconsistentText = IsInconsistentString([](const TextComponent& other) { return other.TextString; }); + + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && inconsistentText); + if (UI::PropertyMultiline("Text String", firstComponent.TextString)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& textComponent = entity.GetComponent(); + textComponent.TextString = firstComponent.TextString; + textComponent.TextHash = std::hash()(textComponent.TextString); + } + } + ImGui::PopItemFlag(); + + { + UI::PropertyAssetReferenceSettings settings; + bool customFont = firstComponent.FontHandle != Font::GetDefaultFont()->Handle; + if (customFont) + { + settings.AdvanceToNextColumn = false; + settings.WidthOffset = ImGui::GetStyle().ItemSpacing.x + 28.0f; + } + + const bool inconsistentFont = IsInconsistentPrimitive([](const TextComponent& other) { return other.FontHandle; }); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && inconsistentFont); + if (UI::PropertyAssetReference("Font", firstComponent.FontHandle, "", nullptr, settings)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().FontHandle = firstComponent.FontHandle; + } + } + ImGui::PopItemFlag(); + + if (customFont) + { + ImGui::SameLine(); + float prevItemHeight = ImGui::GetItemRectSize().y; + if (ImGui::Button("X", { prevItemHeight, prevItemHeight })) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().FontHandle = Font::GetDefaultFont()->Handle; + } + } + ImGui::NextColumn(); + } + } + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TextComponent& other) { return other.Color; })); + if (UI::PropertyColor("Color", firstComponent.Color)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Color = firstComponent.Color; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TextComponent& other) { return other.LineSpacing; })); + if (UI::Property("Line Spacing", firstComponent.LineSpacing, 0.01f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LineSpacing = firstComponent.LineSpacing; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TextComponent& other) { return other.Kerning; })); + if (UI::Property("Kerning", firstComponent.Kerning, 0.01f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Kerning = firstComponent.Kerning; + } + } + ImGui::PopItemFlag(); + + UI::Separator(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TextComponent& other) { return other.MaxWidth; })); + if (UI::Property("Max Width", firstComponent.MaxWidth)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().MaxWidth = firstComponent.MaxWidth; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TextComponent& other) { return other.ScreenSpace; })); + if (UI::Property("Screen Space", firstComponent.ScreenSpace)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ScreenSpace = firstComponent.ScreenSpace; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const TextComponent& other) { return other.DropShadow; })); + if (UI::Property("Drop Shadow", firstComponent.DropShadow)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().DropShadow = firstComponent.DropShadow; + } + } + ImGui::PopItemFlag(); + + if (firstComponent.DropShadow) + { + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TextComponent& other) { return other.ShadowDistance; })); + if (UI::Property("Shadow Distance", firstComponent.ShadowDistance)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ShadowDistance = firstComponent.ShadowDistance; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TextComponent& other) { return other.ShadowColor; })); + if (UI::PropertyColor("Shadow Color", firstComponent.ShadowColor)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ShadowColor = firstComponent.ShadowColor; + } + } + ImGui::PopItemFlag(); + } + + UI::EndPropertyGrid(); + + }, EditorResources::TextIcon); + + DrawComponent("Directional Light", [&](DirectionalLightComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const DirectionalLightComponent& other) { return other.Radiance; })); + if (UI::PropertyColor("Radiance", firstComponent.Radiance)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Radiance = firstComponent.Radiance; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const DirectionalLightComponent& other) { return other.Intensity; })); + if (UI::Property("Intensity", firstComponent.Intensity, 0.1f, 0.0f, 1000.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Intensity = firstComponent.Intensity; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const DirectionalLightComponent& other) { return other.CastShadows; })); + if (UI::Property("Cast Shadows", firstComponent.CastShadows)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().CastShadows = firstComponent.CastShadows; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const DirectionalLightComponent& other) { return other.SoftShadows; })); + if (UI::Property("Soft Shadows", firstComponent.SoftShadows)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().SoftShadows = firstComponent.SoftShadows; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const DirectionalLightComponent& other) { return other.LightSize; })); + if (UI::Property("Source Size", firstComponent.LightSize)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LightSize = firstComponent.LightSize; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::DirectionalLightIcon); + + DrawComponent("Point Light", [&](PointLightComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const PointLightComponent& other) { return other.Radiance; })); + if (UI::PropertyColor("Radiance", firstComponent.Radiance)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Radiance = firstComponent.Radiance; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const PointLightComponent& other) { return other.Intensity; })); + if (UI::Property("Intensity", firstComponent.Intensity, 0.05f, 0.0f, 500.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Intensity = firstComponent.Intensity; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const PointLightComponent& other) { return other.Radius; })); + if (UI::Property("Radius", firstComponent.Radius, 0.1f, 0.0f, std::numeric_limits::max())) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Radius = firstComponent.Radius; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const PointLightComponent& other) { return other.Falloff; })); + if (UI::Property("Falloff", firstComponent.Falloff, 0.005f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Falloff = firstComponent.Falloff; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::PointLightIcon); + + DrawComponent("Spot Light", [&](SpotLightComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.Radiance; })); + if (UI::PropertyColor("Radiance", firstComponent.Radiance)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Radiance = firstComponent.Radiance; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.Intensity; })); + if (UI::Property("Intensity", firstComponent.Intensity, 0.1f, 0.0f, 1000.0f)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Intensity = glm::clamp(firstComponent.Intensity, 0.0f, 1000.0f); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.Angle; })); + if (UI::Property("Angle", firstComponent.Angle)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Angle = glm::clamp(firstComponent.Angle, 0.1f, 180.0f); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.AngleAttenuation; })); + if (UI::Property("Angle Attenuation", firstComponent.AngleAttenuation, 0.01f, 0.0f, std::numeric_limits::max())) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().AngleAttenuation = glm::max(firstComponent.AngleAttenuation, 0.0f); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.Falloff; })); + if (UI::Property("Falloff", firstComponent.Falloff, 0.01f, 0.0f, 1.0f)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Falloff = glm::clamp(firstComponent.Falloff, 0.0f, 1.0f); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.Range; })); + if (UI::Property("Range", firstComponent.Range, 0.1f, 0.0f, std::numeric_limits::max())) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Range = glm::max(firstComponent.Range, 0.0f); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.CastsShadows; })); + if (UI::Property("Cast Shadows", firstComponent.CastsShadows)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().CastsShadows = firstComponent.CastsShadows; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpotLightComponent& other) { return other.SoftShadows; })); + if (UI::Property("Soft Shadows", firstComponent.SoftShadows)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().SoftShadows = firstComponent.SoftShadows; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::SpotLightIcon); + + DrawComponent("Sky Light", [&](SkyLightComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SkyLightComponent& other) { return other.SceneEnvironment; })); + if (UI::PropertyAssetReference("Environment Map", firstComponent.SceneEnvironment)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& slc = entity.GetComponent(); + slc.SceneEnvironment = firstComponent.SceneEnvironment; + slc.DynamicSky = !firstComponent.SceneEnvironment; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SkyLightComponent& other) { return other.Intensity; })); + if (UI::Property("Intensity", firstComponent.Intensity, 0.01f, 0.0f, 5.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Intensity = firstComponent.Intensity; + } + } + ImGui::PopItemFlag(); + + if (AssetManager::IsAssetHandleValid(firstComponent.SceneEnvironment)) + { + Ref environment = AssetManager::GetAssetAsync(firstComponent.SceneEnvironment); + bool lodChanged = false; + if (environment && environment->RadianceMap) + { + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SkyLightComponent& other) + { + Ref otherEnv = AssetManager::GetAssetAsync(other.SceneEnvironment); + return otherEnv->RadianceMap->GetMipLevelCount(); + })); + + lodChanged = UI::PropertySlider("Lod", firstComponent.Lod, 0.0f, static_cast(environment->RadianceMap->GetMipLevelCount())); + ImGui::PopItemFlag(); + } + else + { + UI::BeginDisabled(); + UI::PropertySlider("Lod", firstComponent.Lod, 0.0f, 10.0f); + UI::EndDisabled(); + } + } + + ImGui::Separator(); + + const bool isInconsistentDynamicSky = IsInconsistentPrimitive([](const SkyLightComponent& other) { return other.DynamicSky; }); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && isInconsistentDynamicSky); + if (UI::Property("Dynamic Sky", firstComponent.DynamicSky)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& slc = entity.GetComponent(); + slc.DynamicSky = firstComponent.DynamicSky; + if (slc.DynamicSky) + slc.SceneEnvironment = 0; + } + } + ImGui::PopItemFlag(); + + if (!isInconsistentDynamicSky || !isMultiEdit) + { + bool changed = false; + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SkyLightComponent& other) { return other.TurbidityAzimuthInclination.x; })); + if (UI::Property("Turbidity", firstComponent.TurbidityAzimuthInclination.x, 0.01f, 1.8f, std::numeric_limits::max())) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().TurbidityAzimuthInclination.x = firstComponent.TurbidityAzimuthInclination.x; + } + + changed = true; + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SkyLightComponent& other) { return other.TurbidityAzimuthInclination.y; })); + if (UI::Property("Azimuth", firstComponent.TurbidityAzimuthInclination.y, 0.01f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().TurbidityAzimuthInclination.y = firstComponent.TurbidityAzimuthInclination.y; + } + + changed = true; + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SkyLightComponent& other) { return other.TurbidityAzimuthInclination.z; })); + if (UI::Property("Inclination", firstComponent.TurbidityAzimuthInclination.z, 0.01f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().TurbidityAzimuthInclination.z = firstComponent.TurbidityAzimuthInclination.z; + } + + changed = true; + } + ImGui::PopItemFlag(); + + if (changed) + { + if (auto env = AssetManager::GetMemoryAsset(firstComponent.SceneEnvironment).As(); env) + { + Ref preethamEnv = Renderer::CreatePreethamSky(firstComponent.TurbidityAzimuthInclination.x, firstComponent.TurbidityAzimuthInclination.y, firstComponent.TurbidityAzimuthInclination.z); + env->RadianceMap = preethamEnv; + env->IrradianceMap = preethamEnv; + } + else + { + Ref preethamEnv = Renderer::CreatePreethamSky(firstComponent.TurbidityAzimuthInclination.x, firstComponent.TurbidityAzimuthInclination.y, firstComponent.TurbidityAzimuthInclination.z); + firstComponent.SceneEnvironment = AssetManager::AddMemoryOnlyAsset(Ref::Create(preethamEnv, preethamEnv)); + } + + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().SceneEnvironment = firstComponent.SceneEnvironment; + } + } + } + UI::EndPropertyGrid(); + }, EditorResources::SkyLightIcon); + + DrawComponent("Sprite Renderer", [&](SpriteRendererComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpriteRendererComponent& other) { return other.Color; })); + if (UI::PropertyColor("Color", firstComponent.Color)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Color = firstComponent.Color; + } + } + ImGui::PopItemFlag(); + + { + UI::PropertyAssetReferenceSettings settings; + bool textureSet = firstComponent.Texture != 0; + if (textureSet) + { + settings.AdvanceToNextColumn = false; + settings.WidthOffset = ImGui::GetStyle().ItemSpacing.x + 28.0f; + } + + const bool inconsistentTexture = IsInconsistentPrimitive([](const SpriteRendererComponent& other) { return other.Texture; }); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && inconsistentTexture); + if (UI::PropertyAssetReference("Texture", firstComponent.Texture, "", nullptr, settings)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Texture = firstComponent.Texture; + } + } + ImGui::PopItemFlag(); + + if (textureSet) + { + ImGui::SameLine(); + float prevItemHeight = ImGui::GetItemRectSize().y; + if (ImGui::Button("X", { prevItemHeight, prevItemHeight })) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Texture = 0; + } + } + ImGui::NextColumn(); + } + } + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpriteRendererComponent& other) { return other.TilingFactor; })); + if (UI::Property("Tiling Factor", firstComponent.TilingFactor)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().TilingFactor = firstComponent.TilingFactor; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpriteRendererComponent& other) { return other.UVStart; })); + if (UI::Property("UV Start", firstComponent.UVStart)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().UVStart = firstComponent.UVStart; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpriteRendererComponent& other) { return other.UVEnd; })); + if (UI::Property("UV End", firstComponent.UVEnd)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().UVEnd = firstComponent.UVEnd; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SpriteRendererComponent& other) { return other.ScreenSpace; })); + if (UI::Property("Screen Space", firstComponent.ScreenSpace)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ScreenSpace = firstComponent.ScreenSpace; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::SpriteIcon); + + DrawComponent("Rigidbody 2D", [&](RigidBody2DComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + // Rigidbody2D Type + const char* rb2dTypeStrings[] = { "Static", "Dynamic", "Kinematic" }; + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBody2DComponent& other) { return (int)other.BodyType; })); + if (UI::PropertyDropdown("Type", rb2dTypeStrings, 3, (int*)&firstComponent.BodyType)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().BodyType = firstComponent.BodyType; + } + } + ImGui::PopItemFlag(); + + if (firstComponent.BodyType == RigidBody2DComponent::Type::Dynamic) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBody2DComponent& other) { return other.FixedRotation; })); + if (UI::Property("Fixed Rotation", firstComponent.FixedRotation)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().FixedRotation = firstComponent.FixedRotation; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBody2DComponent& other) { return other.Mass; })); + if (UI::Property("Mass", firstComponent.Mass)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Mass = firstComponent.Mass; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBody2DComponent& other) { return other.LinearDrag; })); + if (UI::Property("Linear Drag", firstComponent.LinearDrag)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LinearDrag = firstComponent.LinearDrag; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBody2DComponent& other) { return other.AngularDrag; })); + if (UI::Property("Angular Drag", firstComponent.AngularDrag)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().AngularDrag = firstComponent.AngularDrag; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBody2DComponent& other) { return other.GravityScale; })); + if (UI::Property("Gravity Scale", firstComponent.GravityScale)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().GravityScale = firstComponent.GravityScale; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBody2DComponent& other) { return other.IsBullet; })); + if (UI::Property("Is Bullet", firstComponent.IsBullet)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().IsBullet = firstComponent.IsBullet; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + } + + UI::EndPropertyGrid(); + }, EditorResources::RigidBody2DIcon); + + DrawComponent("Box Collider 2D", [&](BoxCollider2DComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const BoxCollider2DComponent& other) { return other.Offset; })); + if (UI::Property("Offset", firstComponent.Offset)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Offset = firstComponent.Offset; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const BoxCollider2DComponent& other) { return other.Size; })); + if (UI::Property("Size", firstComponent.Size)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Size = firstComponent.Size; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const BoxCollider2DComponent& other) { return other.Density; })); + if (UI::Property("Density", firstComponent.Density)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Density = firstComponent.Density; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const BoxCollider2DComponent& other) { return other.Friction; })); + if (UI::Property("Friction", firstComponent.Friction)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Friction = firstComponent.Friction; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::BoxCollider2DIcon); + + DrawComponent("Circle Collider 2D", [&](CircleCollider2DComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CircleCollider2DComponent& other) { return other.Offset; })); + if (UI::Property("Offset", firstComponent.Offset)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Offset = firstComponent.Offset; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CircleCollider2DComponent& other) { return other.Radius; })); + if (UI::Property("Radius", firstComponent.Radius)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Radius = firstComponent.Radius; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CircleCollider2DComponent& other) { return other.Density; })); + if (UI::Property("Density", firstComponent.Density)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Density = firstComponent.Density; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CircleCollider2DComponent& other) { return other.Friction; })); + if (UI::Property("Friction", firstComponent.Friction)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Friction = firstComponent.Friction; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::CircleCollider2DIcon); + + DrawComponent("RigidBody", [&](RigidBodyComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + static const char* s_RigidBodyTypeNames[] = { "Static", "Dynamic", "Kinematic"}; + + if (!PhysicsLayerManager::IsLayerValid(firstComponent.LayerID)) + { + for (auto& entityID : entityIDs) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LayerID = 0; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetCollisionLayer(firstComponent.LayerID); + } + } + } + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.BodyType; })); + if (UI::PropertyDropdown("Type", s_RigidBodyTypeNames, 3, firstComponent.BodyType)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& rigidBodyComponent = entity.GetComponent(); + rigidBodyComponent.BodyType = firstComponent.BodyType; + + if (m_Context->IsPlaying() && rigidBodyComponent.EnableDynamicTypeChange) + m_Context->GetPhysicsScene()->SetBodyType(entity, firstComponent.BodyType); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.EnableDynamicTypeChange; })); + if (UI::Property("Enable Dynamic Type Change", firstComponent.EnableDynamicTypeChange, "Set to True if you want to change a static entity to dynamic during runtime.")) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().EnableDynamicTypeChange = firstComponent.EnableDynamicTypeChange; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.IsTrigger; })); + if (UI::Property("Is Trigger", firstComponent.IsTrigger, "A trigger body will detect when another body collides with it, but won't repel the other body, allowing it to act as a collision \"sensor\".")) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().IsTrigger = firstComponent.IsTrigger; + } + } + ImGui::PopItemFlag(); + + const auto& layerNames = PhysicsLayerManager::GetLayerNames(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, entityIDs.size() > 1 && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.LayerID; })); + ImGui::PushItemWidth(125.0f); + uint32_t oldLayer = firstComponent.LayerID; + if (UI::PropertyDropdown("Layer", layerNames, (int32_t)layerNames.size(), (int*)&firstComponent.LayerID)) + { + for (auto& entityID : entityIDs) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LayerID = firstComponent.LayerID; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetCollisionLayer(firstComponent.LayerID); + } + } + } + ImGui::PopItemWidth(); + ImGui::PopItemFlag(); + + if (firstComponent.BodyType == EBodyType::Static) + UI::EndPropertyGrid(); + + if (firstComponent.BodyType != EBodyType::Static) + { + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.Mass; })); + if (UI::Property("Mass", firstComponent.Mass, 0.1f, 1.0f, 100000.0f, "Mass in Kilograms (e.g 1000 for a 1m^3 cube)")) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Mass = firstComponent.Mass; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetMass(firstComponent.Mass); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.LinearDrag; })); + if (UI::Property("Linear Drag", firstComponent.LinearDrag)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LinearDrag = firstComponent.LinearDrag; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetLinearDrag(firstComponent.LinearDrag); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.AngularDrag; })); + if (UI::Property("Angular Drag", firstComponent.AngularDrag)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().AngularDrag = firstComponent.AngularDrag; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetAngularDrag(firstComponent.AngularDrag); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.DisableGravity; })); + if (UI::Property("Disable Gravity", firstComponent.DisableGravity, "If checked this body won't be affected by gravity")) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().DisableGravity = firstComponent.DisableGravity; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetGravityEnabled(!firstComponent.DisableGravity); + } + } + } + ImGui::PopItemFlag(); + + static const char* s_CollisionDetectionNames[] = { "Discrete", "Continuous", "Continuous Speculative" }; + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return (int)other.CollisionDetection; })); + if (UI::PropertyDropdown("Collision Detection", s_CollisionDetectionNames, 3, firstComponent.CollisionDetection)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().CollisionDetection = firstComponent.CollisionDetection; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetCollisionDetectionMode(firstComponent.CollisionDetection); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.InitialLinearVelocity; })); + if (UI::Property("Initial Linear Velocity", firstComponent.InitialLinearVelocity)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().InitialLinearVelocity = firstComponent.InitialLinearVelocity; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.InitialAngularVelocity; })); + if (UI::Property("Initial Angular Velocity", firstComponent.InitialAngularVelocity)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().InitialAngularVelocity = firstComponent.InitialAngularVelocity; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.MaxLinearVelocity; })); + if (UI::Property("Max Linear Velocity", firstComponent.MaxLinearVelocity)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().MaxLinearVelocity = firstComponent.MaxLinearVelocity; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) { return other.MaxAngularVelocity; })); + if (UI::Property("Max Angular Velocity", firstComponent.MaxAngularVelocity)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().MaxAngularVelocity = firstComponent.MaxAngularVelocity; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + + if (UI::BeginTreeNode("Constraints", false)) + { + UI::BeginPropertyGrid(); + + EActorAxis lockedAxes; + + if (m_Context->IsPlaying()) + { + auto physicsBody = m_Context->GetPhysicsScene()->GetBody(firstEntity); + lockedAxes = physicsBody->GetLockedAxes(); + } + else + { + lockedAxes = firstComponent.LockedAxes; + } + + bool translationX = (lockedAxes & EActorAxis::TranslationX) != EActorAxis::None; + bool translationY = (lockedAxes & EActorAxis::TranslationY) != EActorAxis::None; + bool translationZ = (lockedAxes & EActorAxis::TranslationZ) != EActorAxis::None; + bool rotationX = (lockedAxes & EActorAxis::RotationX) != EActorAxis::None; + bool rotationY = (lockedAxes & EActorAxis::RotationY) != EActorAxis::None; + bool rotationZ = (lockedAxes & EActorAxis::RotationZ) != EActorAxis::None; + + UI::BeginCheckboxGroup("Freeze Position"); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) + { + return other.LockedAxes & EActorAxis::TranslationX; + })); + + if (UI::PropertyCheckboxGroup("X", translationX)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (translationX) + component.LockedAxes |= EActorAxis::TranslationX; + else + component.LockedAxes &= ~EActorAxis::TranslationX; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetAxisLock(EActorAxis::Translation, translationX, true); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) + { + return other.LockedAxes & EActorAxis::TranslationY; + })); + if (UI::PropertyCheckboxGroup("Y", translationY)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (translationY) + component.LockedAxes |= EActorAxis::TranslationY; + else + component.LockedAxes &= ~EActorAxis::TranslationY; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetAxisLock(EActorAxis::TranslationY, translationY, true); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) + { + return other.LockedAxes & EActorAxis::TranslationZ; + })); + if (UI::PropertyCheckboxGroup("Z", translationZ)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (translationZ) + component.LockedAxes |= EActorAxis::TranslationZ; + else + component.LockedAxes &= ~EActorAxis::TranslationZ; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetAxisLock(EActorAxis::TranslationZ, translationZ, true); + } + } + } + ImGui::PopItemFlag(); + UI::EndCheckboxGroup(); + + UI::BeginCheckboxGroup("Freeze Rotation"); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) + { + return other.LockedAxes & EActorAxis::RotationX; + })); + if (UI::PropertyCheckboxGroup("X", rotationX)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (rotationX) + component.LockedAxes |= EActorAxis::RotationX; + else + component.LockedAxes &= ~EActorAxis::RotationX; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetAxisLock(EActorAxis::RotationX, rotationX, true); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) + { + return other.LockedAxes & EActorAxis::RotationY; + })); + if (UI::PropertyCheckboxGroup("Y", rotationY)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (rotationY) + component.LockedAxes |= EActorAxis::RotationY; + else + component.LockedAxes &= ~EActorAxis::RotationY; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetAxisLock(EActorAxis::RotationY, rotationY, true); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const RigidBodyComponent& other) + { + return other.LockedAxes & EActorAxis::RotationZ; + })); + + if (UI::PropertyCheckboxGroup("Z", rotationZ)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + + if (rotationZ) + component.LockedAxes |= EActorAxis::RotationZ; + else + component.LockedAxes &= ~EActorAxis::RotationZ; + + if (m_Context->IsPlaying()) + { + auto actor = m_Context->GetPhysicsScene()->GetBody(entity); + actor->SetAxisLock(EActorAxis::RotationZ, rotationZ, true); + } + } + } + ImGui::PopItemFlag(); + UI::EndCheckboxGroup(); + + UI::EndPropertyGrid(); + UI::EndTreeNode(); + UI::ShiftCursorY(-18.0f); + } + } + + }, EditorResources::RigidBodyIcon); + + DrawComponent("Character Controller", [&](CharacterControllerComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + // Layer has been removed, set to Default layer + if (!PhysicsLayerManager::IsLayerValid(firstComponent.LayerID)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LayerID = 0; + } + } + + int layerCount = PhysicsLayerManager::GetLayerCount(); + const auto& layerNames = PhysicsLayerManager::GetLayerNames(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CharacterControllerComponent& other) { return other.LayerID; })); + uint32_t oldLayer = firstComponent.LayerID; + if (UI::PropertyDropdown("Layer", layerNames, layerCount, (int*)&firstComponent.LayerID)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().LayerID = firstComponent.LayerID; + } + } + ImGui::PopItemFlag(); + + auto physicsScene = m_Context->GetPhysicsScene(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CharacterControllerComponent& other) { return other.DisableGravity; })); + if (UI::Property("Disable Gravity", firstComponent.DisableGravity)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().DisableGravity = firstComponent.DisableGravity; + + if (m_Context->IsPlaying()) + { + auto controller = m_Context->GetPhysicsScene()->GetCharacterController(entity); + if (controller) + controller->SetGravityEnabled(!firstComponent.DisableGravity); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const CharacterControllerComponent& other) { return other.ControlMovementInAir; })); + if (UI::Property("Control Movement In Air", firstComponent.ControlMovementInAir)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ControlMovementInAir = firstComponent.ControlMovementInAir; + + if (m_Context->IsPlaying()) + { + auto controller = m_Context->GetPhysicsScene()->GetCharacterController(entity); + if (controller) + controller->SetControlMovementInAir(!firstComponent.ControlMovementInAir); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const CharacterControllerComponent& other) { return other.ControlRotationInAir; })); + if (UI::Property("Control Rotation In Air", firstComponent.ControlRotationInAir)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ControlRotationInAir = firstComponent.ControlRotationInAir; + + if (m_Context->IsPlaying()) + { + auto controller = m_Context->GetPhysicsScene()->GetCharacterController(entity); + if (controller) + controller->SetControlRotationInAir(!firstComponent.ControlRotationInAir); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CharacterControllerComponent& other) { return other.SlopeLimitDeg; })); + if (UI::Property("Slope Limit", firstComponent.SlopeLimitDeg, 1.0f, 0.0f, 90.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().SlopeLimitDeg = firstComponent.SlopeLimitDeg; + + if (m_Context->IsPlaying()) + { + auto controller = physicsScene->GetCharacterController(entity); + if (controller) + controller->SetSlopeLimit(firstComponent.SlopeLimitDeg); + } + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CharacterControllerComponent& other) { return other.StepOffset; })); + if (UI::Property("Step Offset", firstComponent.StepOffset)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().StepOffset = firstComponent.StepOffset; + if (m_Context->IsPlaying()) + { + auto controller = physicsScene->GetCharacterController(entity); + if (controller) + controller->SetStepOffset(firstComponent.StepOffset); + } + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::CharacterControllerIcon); + + DrawComponent("Compound Collider", [&](CompoundColliderComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + UI::Property("Include Static Child Colliders", firstComponent.IncludeStaticChildColliders, "If set to True, any child entity that has a collider, and is static, will have its collider merged into this CompoundCollider."); + UI::Property("Is Immutable", firstComponent.IsImmutable, "An immutable CompoundCollider cannot be modified during runtime, allowing for certain performance optimizations. Set to False if you're planning on adding or removing colliders during runtime."); + + UI::EndPropertyGrid(); + }, EditorResources::CompoundColliderIcon); + + DrawComponent("Box Collider", [&](BoxColliderComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const BoxColliderComponent& other) { return other.HalfSize; })); + if (UI::Property("Half Size", firstComponent.HalfSize, 0.1f, 0.05f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().HalfSize = firstComponent.HalfSize; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const BoxColliderComponent& other) { return other.Offset; })); + if (UI::Property("Offset", firstComponent.Offset)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Offset = firstComponent.Offset; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const BoxColliderComponent& other) { return other.Material.Friction; })); + if (UI::Property("Friction", firstComponent.Material.Friction, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Friction = firstComponent.Material.Friction; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit&& IsInconsistentPrimitive([](const BoxColliderComponent& other) { return other.Material.Restitution; })); + if (UI::Property("Restitution", firstComponent.Material.Restitution, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Restitution = firstComponent.Material.Restitution; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::BoxColliderIcon); + + DrawComponent("Sphere Collider", [&](SphereColliderComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SphereColliderComponent& other) { return other.Radius; })); + if (UI::Property("Radius", firstComponent.Radius)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Radius = firstComponent.Radius; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SphereColliderComponent& other) { return other.Offset; })); + if (UI::Property("Offset", firstComponent.Offset)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Offset = firstComponent.Offset; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SphereColliderComponent& other) { return other.Material.Friction; })); + if (UI::Property("Friction", firstComponent.Material.Friction, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Friction = firstComponent.Material.Friction; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const SphereColliderComponent& other) { return other.Material.Restitution; })); + if (UI::Property("Restitution", firstComponent.Material.Restitution, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Restitution = firstComponent.Material.Restitution; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::SphereColliderIcon); + + DrawComponent("Capsule Collider", [&](CapsuleColliderComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CapsuleColliderComponent& other) { return other.Radius; })); + if (UI::Property("Radius", firstComponent.Radius)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Radius = firstComponent.Radius; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CapsuleColliderComponent& other) { return other.HalfHeight; })); + if (UI::Property("HalfHeight", firstComponent.HalfHeight)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().HalfHeight = firstComponent.HalfHeight; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CapsuleColliderComponent& other) { return other.Offset; })); + if (UI::Property("Offset", firstComponent.Offset)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Offset = firstComponent.Offset; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CapsuleColliderComponent& other) { return other.Material.Friction; })); + if (UI::Property("Friction", firstComponent.Material.Friction, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Friction = firstComponent.Material.Friction; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const CapsuleColliderComponent& other) { return other.Material.Restitution; })); + if (UI::Property("Restitution", firstComponent.Material.Restitution, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Restitution = firstComponent.Material.Restitution; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::CapsuleColliderIcon); + + DrawComponent("Mesh Collider", [&](MeshColliderComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const MeshColliderComponent& other) { return other.ColliderAsset; })); + if (UI::PropertyAssetReference("Collider", firstComponent.ColliderAsset)) + { + const auto& colliderAsset = AssetManager::GetAsset(firstComponent.ColliderAsset); + + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + component.ColliderAsset = firstComponent.ColliderAsset; + + if (colliderAsset) + { + component.UseSharedShape = colliderAsset->AlwaysShareShape; + component.CollisionComplexity = colliderAsset->CollisionComplexity; + } + + if (component.ColliderAsset == 0) + PhysicsSystem::GetOrCreateColliderAsset(entity, component); + + if (entity.HasComponent()) + component.SubmeshIndex = entity.GetComponent().SubmeshIndex; + } + } + ImGui::PopItemFlag(); + + auto colliderAsset = AssetManager::GetAsset(firstComponent.ColliderAsset); + const bool isPhysicalAsset = !AssetManager::GetMemoryAsset(firstComponent.ColliderAsset); + + UI::BeginDisabled(colliderAsset && isPhysicalAsset); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const MeshColliderComponent& other) { return other.UseSharedShape; })); + if (UI::Property("Use Shared Shape", firstComponent.UseSharedShape)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().UseSharedShape = firstComponent.UseSharedShape; + } + } + UI::SetTooltip("Allows this collider to share its collider data. (Default: False)"); + ImGui::PopItemFlag(); + UI::EndDisabled(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const MeshColliderComponent& other) { return other.Material.Friction; })); + if (UI::Property("Friction", firstComponent.Material.Friction, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Friction = firstComponent.Material.Friction; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const MeshColliderComponent& other) { return other.Material.Restitution; })); + if (UI::Property("Restitution", firstComponent.Material.Restitution, 0.1f, 0.0f, 1.0f)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().Material.Restitution = firstComponent.Material.Restitution; + } + } + ImGui::PopItemFlag(); + + UI::BeginDisabled(colliderAsset && isPhysicalAsset); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const MeshColliderComponent& other) + { + return (uint8_t)other.CollisionComplexity; + })); + static const char* s_ColliderUsageOptions[] = { "Default", "Use Complex as Simple", "Use Simple as Complex" }; + if (UI::PropertyDropdown("Collision Complexity", s_ColliderUsageOptions, 3, firstComponent.CollisionComplexity)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& mcc = entity.GetComponent(); + mcc.CollisionComplexity = firstComponent.CollisionComplexity; + + auto collider = AssetManager::GetAsset(mcc.ColliderAsset); + if (collider) + collider->CollisionComplexity = mcc.CollisionComplexity; + } + } + ImGui::PopItemFlag(); + UI::EndDisabled(); + UI::EndPropertyGrid(); + + if (UI::Button("Force cook mesh")) + PhysicsSystem::GetAPI()->GetMeshCookingFactory()->CookMesh(firstComponent.ColliderAsset, true); + }, EditorResources::MeshColliderIcon); + + DrawComponent("Audio Listener", [&](AudioListenerComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + UI::BeginPropertyGrid(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioListenerComponent& other) { return other.Active; })); + if (UI::Property("Active", firstComponent.Active)) + { + auto view = m_Context->GetAllEntitiesWith(); + if (firstComponent.Active == true) + { + for (auto ent : view) + { + Entity e{ ent, m_Context.Raw() }; + e.GetComponent().Active = false; + } + + firstComponent.Active = true; + } + else + { + // Fallback to using main camera as active listener + // - in editor main camera is already the only allowed active listener (may change that in the future) + // - in runtime it falls back to main camera in update loop if can't find other active listener + } + } + ImGui::PopItemFlag(); + + float inAngle = glm::degrees(firstComponent.ConeInnerAngleInRadians); + float outAngle = glm::degrees(firstComponent.ConeOuterAngleInRadians); + float outGain = firstComponent.ConeOuterGain; + //? Have to manually clamp here because UI::Property doesn't take flags to pass in ImGuiSliderFlags_ClampOnInput + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioListenerComponent& other) { return glm::degrees(other.ConeInnerAngleInRadians); })); + if (UI::Property("Inner Cone Angle", inAngle, 1.0f, 0.0f, 360.0f)) + { + if (inAngle > 360.0f) inAngle = 360.0f; + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ConeInnerAngleInRadians = glm::radians(inAngle); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioListenerComponent& other) { return glm::degrees(other.ConeOuterAngleInRadians); })); + if (UI::Property("Outer Cone Angle", outAngle, 1.0f, 0.0f, 360.0f)) + { + if (outAngle > 360.0f) outAngle = 360.0f; + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ConeOuterAngleInRadians = glm::radians(outAngle); + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioListenerComponent& other) { return other.ConeOuterGain; })); + if (UI::Property("Outer Gain", outGain, 0.01f, 0.0f, 1.0f)) + { + if (outGain > 1.0f) outGain = 1.0f; + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().ConeOuterGain = outGain; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + }, EditorResources::AudioListenerIcon); + /* + DrawComponent("Audio", [&](AudioComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + // PropertyGrid consists out of 2 columns, so need to move cursor accordingly + auto propertyGridSpacing = [] + { + ImGui::Spacing(); + ImGui::NextColumn(); + ImGui::NextColumn(); + }; + + // Making separators a little bit less bright to "separate" them visually from the text + auto& colors = ImGui::GetStyle().Colors; + auto oldSCol = colors[ImGuiCol_Separator]; + const float brM = 0.6f; + colors[ImGuiCol_Separator] = ImVec4{ oldSCol.x * brM, oldSCol.y * brM, oldSCol.z * brM, 1.0f }; + + //======================================================= + + //--- Sound Assets and Looping + //---------------------------- + UI::PushID(); + UI::BeginPropertyGrid(); + // Need to wrap this first Property Grid into another ID, + // otherwise there's a conflict with the next Property Grid. + + //=== Audio Objects API + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentString([](const AudioComponent& other) { return other.StartEvent; })); + if (UI::Property("Start Event", firstComponent.StartEvent)) + { + firstComponent.StartCommandID = Audio::CommandID::FromString(firstComponent.StartEvent.c_str()); + + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& component = entity.GetComponent(); + component.StartEvent = firstComponent.StartEvent; + component.StartCommandID = firstComponent.StartCommandID; + } + } + ImGui::PopItemFlag(); + + //===================== + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioComponent& other) { return other.VolumeMultiplier; })); + if (UI::Property("Volume Multiplier", firstComponent.VolumeMultiplier, 0.01f, 0.0f, 1.0f)) //TODO: switch to dBs in the future ? + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().VolumeMultiplier = firstComponent.VolumeMultiplier; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioComponent& other) { return other.PitchMultiplier; })); + if (UI::Property("Pitch Multiplier", firstComponent.PitchMultiplier, 0.01f, 0.0f, 24.0f)) // max pitch 24 is just an arbitrary number here + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().PitchMultiplier = firstComponent.PitchMultiplier; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioComponent& other) { return other.bPlayOnAwake; })); + if (UI::Property("Play on Awake", firstComponent.bPlayOnAwake)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().bPlayOnAwake = firstComponent.bPlayOnAwake; + } + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const AudioComponent& other) { return other.bStopWhenEntityDestroyed; })); + if (UI::Property("Stop When Entity Is Destroyed", firstComponent.bStopWhenEntityDestroyed)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + entity.GetComponent().bStopWhenEntityDestroyed = firstComponent.bStopWhenEntityDestroyed; + } + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + UI::PopID(); + + colors[ImGuiCol_Separator] = oldSCol; + }, EditorResources::AudioIcon);*/ + + DrawComponent("Tile Renderer", [&](TileRendererComponent& firstComponent, const std::vector& entities, const bool isMultiEdit) + { + AssetHandle meshHandle = firstComponent.StaticMesh; + auto mesh = AssetManager::GetAsset(firstComponent.StaticMesh); + + UI::BeginPropertyGrid(); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TileRendererComponent& other) { return other.StaticMesh; })); + UI::PropertyAssetReferenceError error; + if (UI::PropertyAssetReferenceWithConversion("Static Mesh", meshHandle, + [=](Ref meshAsset) + { + if (m_MeshAssetConvertCallback && !isMultiEdit) + m_MeshAssetConvertCallback(m_Context->GetEntityWithUUID(entities[0]), meshAsset); + }, "", &error)) + { + mesh = AssetManager::GetAsset(meshHandle); + + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& trc = entity.GetComponent(); + trc.StaticMesh = meshHandle; + } + } + ImGui::PopItemFlag(); + + if (error == UI::PropertyAssetReferenceError::InvalidMetadata) + { + if (m_InvalidMetadataCallback && !isMultiEdit) + m_InvalidMetadataCallback(m_Context->GetEntityWithUUID(entities[0]), UI::s_PropertyAssetReferenceAssetHandle); + } + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TileRendererComponent& other) { return other.Width; })); + if (UI::Property("Width", firstComponent.Width)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& c = entity.GetComponent(); + c.Width = firstComponent.Width; + c.MaterialIDs.resize(firstComponent.Width* firstComponent.Height); + } + + firstComponent.MaterialIDs.resize(firstComponent.Width * firstComponent.Height); + } + ImGui::PopItemFlag(); + + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, isMultiEdit && IsInconsistentPrimitive([](const TileRendererComponent& other) { return other.Height; })); + if (UI::Property("Height", firstComponent.Height)) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& c = entity.GetComponent(); + c.MaterialIDs.resize(firstComponent.Width * firstComponent.Height); + } + + firstComponent.MaterialIDs.resize(firstComponent.Width * firstComponent.Height); + } + ImGui::PopItemFlag(); + + UI::EndPropertyGrid(); + + UI::Separator(); + + if (mesh) + { + UI::BeginPropertyGrid(); + uint32_t materialCount = (uint32_t)firstComponent.Materials.size(); + if (UI::PropertyInput("Material Count", materialCount, 1, 1, ImGuiInputTextFlags_EnterReturnsTrue)) + { + if (materialCount < 1) + materialCount = 1; + + firstComponent.Materials.resize(materialCount); + + for (size_t i = 0; i < firstComponent.Materials.size(); i++) + { + if (!firstComponent.Materials[i]) + firstComponent.Materials[i] = Ref::Create(); + } + + } + UI::EndPropertyGrid(); + + for (size_t i = 0; i < firstComponent.Materials.size(); i++) + { + ImGui::PushID(i); + + DrawMaterialTableFunc(this, entities, mesh->GetMaterials(), firstComponent.Materials[i], + [i](const TileRendererComponent& component) { return component.Materials[i]; }); + UI::Separator(); + + ImGui::PopID(); + } + } + }, EditorResources::StaticMeshIcon); + } + + void SceneHierarchyPanel::OnExternalEntityDestroyed(Entity entity) + { + if (m_EntityDeletedCallback) + m_EntityDeletedCallback(entity); + } + +} diff --git a/StarEngine/src/StarEngine/Editor/SceneHierarchyPanel.h b/StarEngine/src/StarEngine/Editor/SceneHierarchyPanel.h new file mode 100644 index 00000000..e03afc08 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/SceneHierarchyPanel.h @@ -0,0 +1,437 @@ +#pragma once + +#include "EditorPanel.h" + +#include "StarEngine/Scene/Scene.h" +#include "StarEngine/Renderer/Mesh.h" +#include "StarEngine/Editor/SelectionManager.h" +#include "StarEngine/Editor/EditorResources.h" +#include "StarEngine/ImGui/UICore.h" +#include "StarEngine/ImGui/PropertyGrid.h" +#include "StarEngine/ImGui/CustomTreeNode.h" +#include "StarEngine/Reflection/TypeName.h" + + +namespace StarEngine { + + // NOTE(Peter): Stolen from imgui.h since IM_COL32 isn't available in this header + #define COLOR32(R,G,B,A) (((ImU32)(A)<<24) | ((ImU32)(B)<<16) | ((ImU32)(G)<<8) | ((ImU32)(R)<<0)) + /* + namespace AnimationGraph { + const std::string& GetName(Identifier id); + }*/ + + class SceneHierarchyPanel : public EditorPanel + { + public: + SceneHierarchyPanel() = default; + SceneHierarchyPanel(const Ref& scene, SelectionContext selectionContext, bool isWindow = true); + + virtual void SetSceneContext(const Ref& scene) override; + Ref GetSceneContext() const { return m_Context; } + + void SetEntityDeletedCallback(const std::function& func) { m_EntityDeletedCallback = func; } + void SetMeshAssetConvertCallback(const std::function)>& func) { m_MeshAssetConvertCallback = func; } + void SetInvalidMetadataCallback(const std::function& func) { m_InvalidMetadataCallback = func; } + + void AddEntityContextMenuPlugin(const std::function& func) { m_EntityContextMenuPlugins.push_back(func); } + + virtual void OnImGuiRender(bool& isOpen) override; + virtual void OnEvent(Event& event) override; + + static SelectionContext GetActiveSelectionContext() { return s_ActiveSelectionContext; } + + public: + template + UI::VectorAxis GetInconsistentVectorAxis(GetOtherFunc func) + { + UI::VectorAxis axes = UI::VectorAxis::None; + + const auto& entities = SelectionManager::GetSelections(m_SelectionContext); + + if (entities.size() < 2) + return axes; + + Entity firstEntity = m_Context->GetEntityWithUUID(entities[0]); + const TVectorType& first = func(firstEntity.GetComponent()); + + for (size_t i = 1; i < entities.size(); i++) + { + Entity entity = m_Context->GetEntityWithUUID(entities[i]); + const TVectorType& otherVector = func(entity.GetComponent()); + + if (glm::epsilonNotEqual(otherVector.x, first.x, FLT_EPSILON)) + axes |= UI::VectorAxis::X; + + if (glm::epsilonNotEqual(otherVector.y, first.y, FLT_EPSILON)) + axes |= UI::VectorAxis::Y; + + if constexpr (std::is_same_v || std::is_same_v) + { + if (glm::epsilonNotEqual(otherVector.z, first.z, FLT_EPSILON)) + axes |= UI::VectorAxis::Z; + } + + if constexpr (std::is_same_v) + { + if (glm::epsilonNotEqual(otherVector.w, first.w, FLT_EPSILON)) + axes |= UI::VectorAxis::W; + } + } + + return axes; + } + + template + UI::VectorAxis GetInconsistentVectorAxis(const TVectorType& first, const TVectorType& other) + { + UI::VectorAxis axes = UI::VectorAxis::None; + + if (glm::epsilonNotEqual(other.x, first.x, FLT_EPSILON)) + axes |= UI::VectorAxis::X; + + if (glm::epsilonNotEqual(other.y, first.y, FLT_EPSILON)) + axes |= UI::VectorAxis::Y; + + if constexpr (std::is_same_v || std::is_same_v) + { + if (glm::epsilonNotEqual(other.z, first.z, FLT_EPSILON)) + axes |= UI::VectorAxis::Z; + } + + if constexpr (std::is_same_v) + { + if (glm::epsilonNotEqual(other.w, first.w, FLT_EPSILON)) + axes |= UI::VectorAxis::W; + } + + return axes; + } + + template + bool IsInconsistentRef(GetOtherFunc func) + { + const auto& entities = SelectionManager::GetSelections(m_SelectionContext); + + if (entities.size() < 2) + return false; + + Entity firstEntity = m_Context->GetEntityWithUUID(entities[0]); + const Ref& first = func(firstEntity.GetComponent()); + + for (size_t i = 1; i < entities.size(); i++) + { + Entity entity = m_Context->GetEntityWithUUID(entities[i]); + const Ref& otherValue = func(entity.GetComponent()); + + if (otherValue != first) + return true; + } + + return false; + } + + template + bool IsInconsistentPrimitive(GetOtherFunc func) + { + const auto& entities = SelectionManager::GetSelections(m_SelectionContext); + + if (entities.size() < 2) + return false; + + Entity firstEntity = m_Context->GetEntityWithUUID(entities[0]); + const TPrimitive& first = func(firstEntity.GetComponent()); + + for (size_t i = 1; i < entities.size(); i++) + { + Entity entity = m_Context->GetEntityWithUUID(entities[i]); + + if (!entity.HasComponent()) + continue; + + const auto& otherValue = func(entity.GetComponent()); + if (otherValue != first) + return true; + } + + return false; + } + + template + bool IsInconsistentString(GetOtherFunc func) + { + return IsInconsistentPrimitive(func); + } + + template + void DrawComponent(const std::string& name, UIFunction uiFunction, Ref icon = nullptr) + { + bool shouldDraw = true; + + auto& entities = SelectionManager::GetSelections(s_ActiveSelectionContext); + for (const auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + if (!entity.HasComponent()) + { + shouldDraw = false; + break; + } + } + + if (!shouldDraw || entities.size() == 0) + return; + + if (icon == nullptr) + icon = EditorResources::AssetIcon; + + // NOTE(Peter): + // This fixes an issue where the first "+" button would display the "Remove" buttons for ALL components on an Entity. + // This is due to ImGui::TreeNodeEx only pushing the id for it's children if it's actually open + ImGui::PushID((void*)typeid(TComponent).hash_code()); + ImVec2 contentRegionAvailable = ImGui::GetContentRegionAvail(); + + bool open = UI::TreeNodeWithIcon(name, icon, { 14.0f, 14.0f }); + bool right_clicked = ImGui::IsItemClicked(ImGuiMouseButton_Right); + float lineHeight = ImGui::GetItemRectMax().y - ImGui::GetItemRectMin().y; + float itemPadding = 4.0f; + + bool resetValues = false; + bool removeComponent = false; + + //if constexpr (!std::is_same_v) + //{ + + ImGui::SameLine(contentRegionAvailable.x - lineHeight - 5.0f); + UI::ShiftCursorY(lineHeight / 4.0f); + if (ImGui::InvisibleButton("##options", ImVec2{ lineHeight, lineHeight }) || right_clicked) + { + ImGui::OpenPopup("ComponentSettings"); + } + UI::DrawButtonImage(EditorResources::GearIcon, COLOR32(160, 160, 160, 200), + COLOR32(160, 160, 160, 255), + COLOR32(160, 160, 160, 150), + UI::RectExpanded(UI::GetItemRect(), 0.0f, 0.0f)); + + if (UI::BeginPopup("ComponentSettings")) + { + Entity entity = m_Context->GetEntityWithUUID(entities[0]); + auto& component = entity.GetComponent(); + + if constexpr (!std::is_same_v) + { + UI::ShiftCursorX(itemPadding); + if (ImGui::MenuItem("Copy")) + Scene::CopyComponentFromScene(m_ComponentCopyEntity, m_ComponentCopyScene, entity, m_Context); + + UI::ShiftCursorX(itemPadding); + if (ImGui::MenuItem("Paste")) + { + for (auto entityID : SelectionManager::GetSelections(s_ActiveSelectionContext)) + { + Entity selectedEntity = m_Context->GetEntityWithUUID(entityID); + Scene::CopyComponentFromScene(selectedEntity, m_Context, m_ComponentCopyEntity, m_ComponentCopyScene); + } + } + } + + UI::ShiftCursorX(itemPadding); + if (ImGui::MenuItem("Reset")) + resetValues = true; + + UI::ShiftCursorX(itemPadding); + if constexpr (!std::is_same_v && !std::is_same_v) + { + if (ImGui::MenuItem("Remove component")) + removeComponent = true; + } + + UI::EndPopup(); + } + //} + + if (open) + { + Entity entity = m_Context->GetEntityWithUUID(entities[0]); + TComponent& firstComponent = entity.GetComponent(); + const bool isMultiEdit = entities.size() > 1; + uiFunction(firstComponent, entities, isMultiEdit); + ImGui::TreePop(); + } + + if (removeComponent) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + if (entity.HasComponent()) + { + if constexpr (std::is_same_v) + { + auto& mc = entity.GetComponent(); + mc.Mesh = 0; + m_Context->RebuildMeshEntityHierarchy(entity); + } + entity.RemoveComponent(); + } + } + } + + if (resetValues) + { + for (auto& entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + if (entity.HasComponent()) + { + if constexpr (std::is_same_v) + { + // For submeshComponent reset, we must preserve mesh, submeshindex, and bone entity ids + // (the only thing that gets reset is the material table) + auto oldComponent = entity.GetComponent(); + entity.RemoveComponent(); + auto& component = entity.AddComponent(); + component.Mesh = oldComponent.Mesh; + component.SubmeshIndex = oldComponent.SubmeshIndex; + component.BoneEntityIds = oldComponent.BoneEntityIds; + } + else + { + entity.RemoveComponent(); + entity.AddComponent(); + if constexpr (std::is_same_v) + { + m_Context->RebuildMeshEntityHierarchy(entity); + } + } + } + } + } + + if (!open) + UI::ShiftCursorY(-(ImGui::GetStyle().ItemSpacing.y + 1.0f)); + + ImGui::PopID(); + } + + private: + + void DrawEntityCreateMenu(Entity parent = {}); + void DrawEntityNode(Entity entity, const std::string& searchFilter = {}); + void DrawComponents(const std::vector& entities); + /* + template + void EditGraphInput(Identifier id, choc::value::ValueView& value, const std::vector& entities) + { + auto t = value.get(); + if (UI::Property(AnimationGraph::GetName(id).c_str(), t)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& anim = entity.GetComponent(); + if (anim.AnimationGraph) + { + auto instanceValue = anim.AnimationGraph->InValue(id); + instanceValue.set(t); + } + } + } + } + + template<> + void EditGraphInput(Identifier id, choc::value::ValueView& value, const std::vector& entities) + { + AssetHandle handle = value.get(); + if (UI::PropertyAssetReference(AnimationGraph::GetName(id).c_str(), handle)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& anim = entity.GetComponent(); + if (anim.AnimationGraph) + { + auto instanceValue = anim.AnimationGraph->InValue(id); + instanceValue.set(static_cast(handle)); + } + } + } + }*/ + + void OnExternalEntityDestroyed(Entity entity); + + bool TagSearchRecursive(Entity entity, std::string_view searchFilter, uint32_t maxSearchDepth, uint32_t currentDepth = 1); + + private: + Ref m_Context; + bool m_IsWindow; + bool m_IsWindowFocused = false; + bool m_IsHierarchyOrPropertiesFocused = false; + bool m_ShiftSelectionRunning = false; + + std::function m_EntityDeletedCallback; + std::function)> m_MeshAssetConvertCallback; + std::function m_InvalidMetadataCallback; + std::vector> m_EntityContextMenuPlugins; + + SelectionContext m_SelectionContext; + + int32_t m_FirstSelectedRow = -1; + int32_t m_LastSelectedRow = -1; + + Ref m_ComponentCopyScene; + Entity m_ComponentCopyEntity; + + static SelectionContext s_ActiveSelectionContext; + }; + + /* + template<> + inline void SceneHierarchyPanel::EditGraphInput(Identifier id, choc::value::ValueView& value, const std::vector& entities) + { + bool b = false; + if (UI::PropertyButton(AnimationGraph::GetName(id).c_str(), "Trigger")) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& anim = entity.GetComponent(); + if (anim.AnimationGraph) + { + anim.AnimationGraph->InEvent(id)(id); + } + } + } + } + + + template<> + inline void SceneHierarchyPanel::EditGraphInput(Identifier id, choc::value::ValueView& value, const std::vector& entities) + { + glm::vec3 v; + if (value.isObjectWithClassName(type::type_name())) + { + v = *static_cast(value.getRawData()); + } + else + { + SE_CORE_ASSERT(false, "value is not glm::vec3"); + } + if (UI::Property(AnimationGraph::GetName(id).c_str(), v)) + { + for (auto entityID : entities) + { + Entity entity = m_Context->GetEntityWithUUID(entityID); + auto& anim = entity.GetComponent(); + if (anim.AnimationGraph) + { + auto instanceValue = anim.AnimationGraph->InValue(id); + instanceValue.getObjectMemberAt(0).value.set(v.x); + instanceValue.getObjectMemberAt(1).value.set(v.y); + instanceValue.getObjectMemberAt(2).value.set(v.z); + } + } + } + }*/ + +} diff --git a/StarEngine/src/StarEngine/Editor/SelectionManager.cpp b/StarEngine/src/StarEngine/Editor/SelectionManager.cpp new file mode 100644 index 00000000..5d2edfa0 --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/SelectionManager.cpp @@ -0,0 +1,123 @@ +#include "sepch.h" +#include "SelectionManager.h" +#include "StarEngine/Core/Application.h" +#include "StarEngine/Core/Events/SceneEvents.h" + +namespace StarEngine { + + void SelectionManager::Select(SelectionContext contextID, UUID selectionID) + { + auto& contextSelections = s_Contexts[contextID]; + if (std::find(contextSelections.begin(), contextSelections.end(), selectionID) != contextSelections.end()) + return; + + // TODO: Maybe verify if the selectionID is already selected in another context? + + contextSelections.push_back(selectionID); + Application::Get().DispatchEvent(contextID, selectionID, true); + } + + bool SelectionManager::IsSelected(UUID selectionID) + { + for (const auto& [contextID, contextSelections] : s_Contexts) + { + if (std::find(contextSelections.begin(), contextSelections.end(), selectionID) != contextSelections.end()) + { + return true; + } + } + + return false; + } + + bool SelectionManager::IsSelected(SelectionContext contextID, UUID selectionID) + { + const auto& contextSelections = s_Contexts[contextID]; + return std::find(contextSelections.begin(), contextSelections.end(), selectionID) != contextSelections.end(); + } + + bool SelectionManager::IsEntityOrAncestorSelected(const Entity entity) + { + Entity e = entity; + while (e) + { + if (IsSelected(e.GetUUID())) + { + return true; + } + e = e.GetParent(); + } + return false; + } + + bool SelectionManager::IsEntityOrAncestorSelected(SelectionContext contextID, const Entity entity) + { + Entity e = entity; + while (e) + { + if (IsSelected(contextID, e.GetUUID())) + { + return true; + } + e = e.GetParent(); + } + return false; + } + + void SelectionManager::Deselect(UUID selectionID) + { + for (auto& [contextID, contextSelections] : s_Contexts) + { + auto it = std::find(contextSelections.begin(), contextSelections.end(), selectionID); + if (it == contextSelections.end()) + continue; + + Application::Get().DispatchEvent(contextID, selectionID, false); + contextSelections.erase(it); + break; + } + } + + void SelectionManager::Deselect(SelectionContext contextID, UUID selectionID) + { + auto& contextSelections = s_Contexts[contextID]; + auto it = std::find(contextSelections.begin(), contextSelections.end(), selectionID); + if (it == contextSelections.end()) + return; + + contextSelections.erase(it); + } + + void SelectionManager::DeselectAll() + { + for (auto& [ctxID, contextSelections] : s_Contexts) + { + for (const auto& selectionID : contextSelections) + Application::Get().DispatchEvent(ctxID, selectionID, false); + contextSelections.clear(); + } + } + + void SelectionManager::DeselectAll(SelectionContext contextID) + { + auto& contextSelections = s_Contexts[contextID]; + + for (const auto& selectionID : contextSelections) + Application::Get().DispatchEvent(contextID, selectionID, false); + + contextSelections.clear(); + } + + UUID SelectionManager::GetSelection(SelectionContext context, size_t index) + { + auto& contextSelections = s_Contexts[context]; + SE_CORE_VERIFY(index >= 0 && index < contextSelections.size()); + return contextSelections[index]; + } + + size_t SelectionManager::GetSelectionCount(SelectionContext contextID) + { + return s_Contexts[contextID].size(); + } + +} diff --git a/StarEngine/src/StarEngine/Editor/SelectionManager.h b/StarEngine/src/StarEngine/Editor/SelectionManager.h new file mode 100644 index 00000000..ec35326a --- /dev/null +++ b/StarEngine/src/StarEngine/Editor/SelectionManager.h @@ -0,0 +1,35 @@ +#pragma once + +#include "StarEngine/Scene/Scene.h" + +#include + +namespace StarEngine { + + enum class SelectionContext + { + Global = 0, Scene, ContentBrowser, PrefabEditor + }; + + class SelectionManager + { + public: + static void Select(SelectionContext context, UUID selectionID); + static bool IsSelected(UUID selectionID); + static bool IsSelected(SelectionContext context, UUID selectionID); + static bool IsEntityOrAncestorSelected(const Entity entity); + static bool IsEntityOrAncestorSelected(SelectionContext context, const Entity entity); + static void Deselect(UUID selectionID); + static void Deselect(SelectionContext context, UUID selectionID); + static void DeselectAll(); + static void DeselectAll(SelectionContext context); + static UUID GetSelection(SelectionContext context, size_t index); + + static size_t GetSelectionCount(SelectionContext contextID); + inline static const std::vector& GetSelections(SelectionContext context) { return s_Contexts[context]; } + + private: + inline static std::unordered_map> s_Contexts; + }; + +} diff --git a/StarEngine/src/StarEngine/EntryPoint.h b/StarEngine/src/StarEngine/EntryPoint.h new file mode 100644 index 00000000..44c78704 --- /dev/null +++ b/StarEngine/src/StarEngine/EntryPoint.h @@ -0,0 +1,40 @@ +#pragma once + +#include "StarEngine/Core/Application.h" + +extern StarEngine::Application* StarEngine::CreateApplication(int argc, char** argv); +bool g_ApplicationRunning = true; + +namespace StarEngine { + + int Main(int argc, char** argv) + { + while (g_ApplicationRunning) + { + InitializeCore(); + Application* app = CreateApplication(argc, argv); + SE_CORE_ASSERT(app, "Client Application is null!"); + app->Run(); + delete app; + ShutdownCore(); + } + return 0; + } + +} + +#if SE_DIST && SE_PLATFORM_WINDOWS + +int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow) +{ + return StarEngine::Main(__argc, __argv); +} + +#else + +int main(int argc, char** argv) +{ + return StarEngine::Main(argc, argv); +} + +#endif // SE_DIST diff --git a/StarEngine/src/StarEngine/Events/ApplicationEvent.h b/StarEngine/src/StarEngine/Events/ApplicationEvent.h deleted file mode 100644 index bf081459..00000000 --- a/StarEngine/src/StarEngine/Events/ApplicationEvent.h +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once - -#include "StarEngine/Events/Event.h" - -namespace StarEngine { - class WindowResizeEvent : public Event - { - public: - WindowResizeEvent(unsigned int width, unsigned int height) - : m_Width(width), m_Height(height) { - } - - unsigned int GetWidth() const { return m_Width; } - unsigned int GetHeight() const { return m_Height; } - - std::string ToString() const override - { - std::stringstream ss; - ss << "WindowResizeEvent: " << m_Width << ", " << m_Height; - return ss.str(); - } - - EVENT_CLASS_TYPE(WindowResize) - EVENT_CLASS_CATEGORY(EventCategoryApplication) - - private: - unsigned int m_Width, m_Height; - }; - - class WindowCloseEvent : public Event - { - public: - WindowCloseEvent() = default; - - EVENT_CLASS_TYPE(WindowClose) - EVENT_CLASS_CATEGORY(EventCategoryApplication) - }; - - class AppTickEvent : public Event - { - public: - AppTickEvent() = default; - - EVENT_CLASS_TYPE(AppTick) - EVENT_CLASS_CATEGORY(EventCategoryApplication) - }; - - class AppUpdateEvent : public Event - { - public: - AppUpdateEvent() = default; - - EVENT_CLASS_TYPE(AppUpdate) - EVENT_CLASS_CATEGORY(EventCategoryApplication) - }; - - class AppRenderEvent : public Event - { - public: - AppRenderEvent() = default; - - EVENT_CLASS_TYPE(AppRender) - EVENT_CLASS_CATEGORY(EventCategoryApplication) - }; - - class WindowDropEvent : public Event - { - public: - WindowDropEvent(const std::vector& paths) - : m_Paths(paths) { - } - - WindowDropEvent(std::vector&& paths) - : m_Paths(std::move(paths)) { - } - - const std::vector& GetPaths() const { return m_Paths; } - - EVENT_CLASS_TYPE(WindowDrop) - EVENT_CLASS_CATEGORY(EventCategoryApplication) - private: - std::vector m_Paths; - }; -} diff --git a/StarEngine/src/StarEngine/ImGui/Colors.cpp b/StarEngine/src/StarEngine/ImGui/Colors.cpp new file mode 100644 index 00000000..d106f3e2 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/Colors.cpp @@ -0,0 +1,2 @@ +#include +#include "Colors.h" diff --git a/StarEngine/src/StarEngine/ImGui/Colors.h b/StarEngine/src/StarEngine/ImGui/Colors.h new file mode 100644 index 00000000..d9fedc12 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/Colors.h @@ -0,0 +1,35 @@ +#pragma once +#include + +namespace Colors +{ + // To experiment with editor theme live you can change these constexpr into static + // members of a static "Theme" class and add a quick ImGui window to adjust the colour values + namespace Theme + { + constexpr auto accent = IM_COL32(236, 158, 36, 255); + constexpr auto highlight = IM_COL32(39, 185, 242, 255); + constexpr auto niceBlue = IM_COL32(83, 232, 254, 255); + constexpr auto compliment = IM_COL32(78, 151, 166, 255); + constexpr auto background = IM_COL32(36, 36, 36, 255); + constexpr auto backgroundDark = IM_COL32(26, 26, 26, 255); + constexpr auto titlebar = IM_COL32(21, 21, 21, 255); + constexpr auto titlebarOrange = IM_COL32(186, 66, 30, 255); + constexpr auto titlebarGreen = IM_COL32(18, 88, 30, 255); + constexpr auto titlebarRed = IM_COL32(185, 30, 30, 255); + constexpr auto propertyField = IM_COL32(15, 15, 15, 255); + constexpr auto text = IM_COL32(192, 192, 192, 255); + constexpr auto textBrighter = IM_COL32(210, 210, 210, 255); + constexpr auto textDarker = IM_COL32(128, 128, 128, 255); + constexpr auto textError = IM_COL32(230, 51, 51, 255); + constexpr auto muted = IM_COL32(77, 77, 77, 255); + constexpr auto groupHeader = IM_COL32(47, 47, 47, 255); + constexpr auto selection = IM_COL32(237, 192, 119, 255); + constexpr auto selectionMuted = IM_COL32(237, 201, 142, 23); + constexpr auto backgroundPopup = IM_COL32(50, 50, 50, 255); + constexpr auto validPrefab = IM_COL32(82, 179, 222, 255); + constexpr auto invalidPrefab = IM_COL32(222, 43, 43, 255); + constexpr auto missingMesh = IM_COL32(230, 102, 76, 255); + constexpr auto meshNotSet = IM_COL32(250, 101, 23, 255); + } +} diff --git a/StarEngine/src/StarEngine/ImGui/CustomTreeNode.cpp b/StarEngine/src/StarEngine/ImGui/CustomTreeNode.cpp new file mode 100644 index 00000000..e648825a --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/CustomTreeNode.cpp @@ -0,0 +1,297 @@ +#include "sepch.h" +#include "CustomTreeNode.h" + +#include "StarEngine/ImGui/Colors.h" +#include "StarEngine/ImGui/ImGuiUtilities.h" +#include "StarEngine/ImGui/UICore.h" + +namespace ImGui { + + bool TreeNodeWithIcon(StarEngine::Ref icon, ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end, ImColor iconTint/* = IM_COL32_WHITE*/) + { + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + ImGuiLastItemData& lastItem = g.LastItemData; + const ImGuiStyle& style = g.Style; + const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; + const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y)); + + if (!label_end) + label_end = FindRenderedTextEnd(label); + const ImVec2 label_size = CalcTextSize(label, label_end, false); + + // We vertically grow up to current line height up the typical widget height. + const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); + ImRect frame_bb; + frame_bb.Min.x = (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; + frame_bb.Min.y = window->DC.CursorPos.y; + frame_bb.Max.x = window->WorkRect.Max.x; + frame_bb.Max.y = window->DC.CursorPos.y + frame_height; + if (display_frame) + { + // Framed header expand a little outside the default padding, to the edge of InnerClipRect + // (FIXME: May remove this at some point and make InnerClipRect align with WindowPadding.x instead of WindowPadding.x*0.5f) + frame_bb.Min.x -= IM_FLOOR(window->WindowPadding.x * 0.5f - 1.0f); + frame_bb.Max.x += IM_FLOOR(window->WindowPadding.x * 0.5f); + } + + const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapser arrow width + Spacing + const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it + const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapser + ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); + ItemSize(ImVec2(text_width, frame_height), padding.y); + + // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing + ImRect interact_bb = frame_bb; + if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth)) == 0) + interact_bb.Max.x = frame_bb.Min.x + text_width + style.ItemSpacing.x * 2.0f; + + // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. + // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). + // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. + const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; + bool is_open = TreeNodeUpdateNextOpen(id, flags); + + // NOTE(Yan): TreeJumpToParentOnPopMask was removed, not sure if we still need this + // if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + // window->DC.TreeJumpToParentOnPopMask |= (1 << window->DC.TreeDepth); + + bool item_add = ItemAdd(interact_bb, id); + lastItem.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; + lastItem.DisplayRect = frame_bb; + + if (!item_add) + { + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushOverrideID(id); + IMGUI_TEST_ENGINE_ITEM_INFO(lastItem.ID, label, lastItem.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + return is_open; + } + + ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None; + if (flags & ImGuiTreeNodeFlags_AllowOverlap) + button_flags |= ImGuiButtonFlags_AllowOverlap; + if (!is_leaf) + button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + + // We allow clicking on the arrow section with keyboard modifiers held, in order to easily + // allow browsing a tree while preserving selection with code implementing multi-selection patterns. + // When clicking on the rest of the tree node we always disallow keyboard modifiers. + const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x; + const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x; + const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2); + if (window != g.HoveredWindow || !is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_NoKeyModsAllowed; + + // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags. + // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support. + // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1) + // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1) + // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0) + // It is rather standard that arrow click react on Down rather than Up. + // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work. + if (is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + + bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; + const bool was_selected = selected; + + bool hovered, held; + bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); + bool toggled = false; + if (!is_leaf) + { + if (pressed && g.DragDropHoldJustPressedId != id) + { + if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id)) + toggled = true; + if (flags & ImGuiTreeNodeFlags_OpenOnArrow) + toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job + if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseDoubleClicked[0]) + toggled = true; + } + else if (pressed && g.DragDropHoldJustPressedId == id) + { + IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold); + if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. + toggled = true; + } + + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open) + { + toggled = true; + NavMoveRequestCancel(); + } + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? + { + toggled = true; + NavMoveRequestCancel(); + } + + if (toggled) + { + is_open = !is_open; + window->DC.StateStorage->SetInt(id, is_open); + lastItem.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen; + } + } + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) + SetItemAllowOverlap(); + + // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger. + if (selected != was_selected) //-V547 + lastItem.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + // Render + + const ImU32 arrow_col = selected ? Colors::Theme::backgroundDark : Colors::Theme::muted; + + ImGuiNavRenderCursorFlags nav_highlight_flags = ImGuiNavRenderCursorFlags_Compact; + if (display_frame) + { + // Framed type + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : (hovered && !selected && !held && !pressed && !toggled) ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding); + RenderNavCursor(frame_bb, id, nav_highlight_flags); + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), arrow_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), arrow_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); + else // Leaf without bullet, left-adjusted text + text_pos.x -= text_offset_x; + if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton) + frame_bb.Max.x -= g.FontSize + style.FramePadding.x; + + //! Draw icon + if (icon) + { + // Store item data + auto itemId = lastItem.ID; + auto itemFlags = lastItem.ItemFlags; + auto itemStatusFlags = lastItem.StatusFlags; + auto itemRect = lastItem.Rect; + + // Draw icon image which messes up last item data + const float pad = 3.0f; + const float arrowWidth = 20.0f + 1.0f; + auto cursorPos = ImGui::GetCursorPos(); + StarEngine::UI::ShiftCursorY(-frame_height + pad); + StarEngine::UI::ShiftCursorX(arrowWidth); + StarEngine::UI::Image(icon, { frame_height - pad * 2.0f, frame_height - pad * 2.0f }, ImVec2(0, 0), ImVec2(1, 1), iconTint /*selected ? colourDark : tintFloat*/); + + // Restore itme data + ImGui::SetLastItemData(itemId, itemFlags, itemStatusFlags, itemRect); + + text_pos.x += frame_height + 2.0f; + } + + text_pos.y -= 1.0f; + + + + if (g.LogEnabled) + { + // NB: '##' is normally used to hide text (as a library-wide feature), so we need to specify the text range to make sure the ## aren't stripped out here. + const char log_prefix[] = "\n##"; + const char log_suffix[] = "##"; + LogRenderedText(&text_pos, log_prefix, log_prefix + 3); + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + LogRenderedText(&text_pos, log_suffix, log_suffix + 2); + } + else + { + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + } + } + else + { + // Unframed typed for tree nodes + if (hovered || selected) + { + //if (held && hovered) SE_CORE_WARN("held && hovered"); + //if(hovered && !selected && !held && !pressed && !toggled) SE_CORE_WARN("hovered && !selected && !held"); + //else if(!selected) SE_CORE_WARN("ImGuiCol_Header"); + + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : (hovered && !selected && !held && !pressed && !toggled) ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false); + RenderNavHighlight(frame_bb, id, nav_highlight_flags); + } + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), arrow_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), arrow_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); + + //! Draw icon + if (icon) + { + // Store item data + auto itemId = lastItem.ID; + auto itemFlags = lastItem.ItemFlags; + auto itemStatusFlags = lastItem.StatusFlags; + auto itemRect = lastItem.Rect; + + // Draw icon image which messes up last item data + const float pad = 3.0f; + const float arrowWidth = 20.0f + 1.0f; + auto cursorPos = ImGui::GetCursorPos(); + StarEngine::UI::ShiftCursorY(-frame_height + pad); + StarEngine::UI::ShiftCursorX(arrowWidth); + StarEngine::UI::Image(icon, { frame_height - pad * 2.0f, frame_height - pad * 2.0f }, ImVec2(0, 0), ImVec2(1, 1), iconTint /*selected ? colourDark : tintFloat*/); + + // Restore itme data + ImGui::SetLastItemData(itemId, itemFlags, itemStatusFlags, itemRect); + + text_pos.x += frame_height + 2.0f; + } + + text_pos.y -= 1.0f; + + + if (g.LogEnabled) + LogRenderedText(&text_pos, ">"); + RenderText(text_pos, label, label_end, false); + } + + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushOverrideID(id); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + return is_open; + } + + bool TreeNodeWithIcon(StarEngine::Ref icon, const void* ptr_id, ImGuiTreeNodeFlags flags, ImColor iconTint, const char* fmt, ...) + { + va_list args; + va_start(args, fmt); + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const char* label_end = g.TempBuffer.Data + ImFormatStringV(g.TempBuffer.Data, g.TempBuffer.Size, fmt, args); + + bool is_open = TreeNodeWithIcon(icon, window->GetID(ptr_id), flags, g.TempBuffer.Data, label_end, iconTint); + + va_end(args); + return is_open; + } + + bool TreeNodeWithIcon(StarEngine::Ref icon, const char* label, ImGuiTreeNodeFlags flags, ImColor iconTint /*= IM_COL32_WHITE*/) + { + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + return TreeNodeWithIcon(icon, window->GetID(label), flags, label, NULL, iconTint); + } + +} // namespace ImGui diff --git a/StarEngine/src/StarEngine/ImGui/CustomTreeNode.h b/StarEngine/src/StarEngine/ImGui/CustomTreeNode.h new file mode 100644 index 00000000..e63524e5 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/CustomTreeNode.h @@ -0,0 +1,16 @@ +#pragma once + +#include "StarEngine/Core/Ref.h" + +#include + +namespace StarEngine { + class Texture2D; +} + +namespace ImGui { + bool TreeNodeWithIcon(StarEngine::Ref icon, ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end, ImColor iconTint = IM_COL32_WHITE); + bool TreeNodeWithIcon(StarEngine::Ref icon, const void* ptr_id, ImGuiTreeNodeFlags flags, ImColor iconTint, const char* fmt, ...); + bool TreeNodeWithIcon(StarEngine::Ref icon, const char* label, ImGuiTreeNodeFlags flags, ImColor iconTint = IM_COL32_WHITE); + +} // namespace UI diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiBuild.cpp b/StarEngine/src/StarEngine/ImGui/ImGuiBuild.cpp index 9eadc3a8..8dfeb742 100644 --- a/StarEngine/src/StarEngine/ImGui/ImGuiBuild.cpp +++ b/StarEngine/src/StarEngine/ImGui/ImGuiBuild.cpp @@ -1,7 +1,3 @@ #include "sepch.h" -#include - -#define IMGUI_IMPL_OPENGL_LOADER_GLAD -#include -#include +#include "backends/imgui_impl_glfw.h" diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiFonts.cpp b/StarEngine/src/StarEngine/ImGui/ImGuiFonts.cpp new file mode 100644 index 00000000..881bba80 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiFonts.cpp @@ -0,0 +1,52 @@ +#include "sepch.h" +#include "ImGuiFonts.h" + +namespace StarEngine::UI { + + static std::unordered_map s_Fonts; + + void Fonts::Add(const FontConfiguration& config, bool isDefault) + { + if (s_Fonts.find(config.FontName) != s_Fonts.end()) + { + SE_CORE_WARN_TAG("EditorUI", "Tried to add font with name '{0}' but that name is already taken!", config.FontName); + return; + } + + ImFontConfig imguiFontConfig; + imguiFontConfig.MergeMode = config.MergeWithLast; + auto& io = ImGui::GetIO(); + ImFont* font = io.Fonts->AddFontFromFileTTF(config.FilePath.data(), config.Size, &imguiFontConfig, config.GlyphRanges == nullptr ? io.Fonts->GetGlyphRangesDefault() : config.GlyphRanges); + SE_CORE_VERIFY(font, "Failed to load font file!"); + s_Fonts[config.FontName] = font; + + if (isDefault) + io.FontDefault = font; + } + + ImFont* Fonts::Get(const std::string& fontName) + { + SE_CORE_VERIFY(s_Fonts.find(fontName) != s_Fonts.end(), "Failed to find font with that name!"); + return s_Fonts.at(fontName); + } + + void Fonts::PushFont(const std::string& fontName) + { + const auto& io = ImGui::GetIO(); + + if (s_Fonts.find(fontName) == s_Fonts.end()) + { + ImGui::PushFont(io.FontDefault); + return; + } + + ImGui::PushFont(s_Fonts.at(fontName)); + } + + void Fonts::PopFont() + { + ImGui::PopFont(); + } + +} + diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiFonts.h b/StarEngine/src/StarEngine/ImGui/ImGuiFonts.h new file mode 100644 index 00000000..074815e6 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiFonts.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace StarEngine::UI { + + struct FontConfiguration + { + std::string FontName; + std::string_view FilePath; + float Size = 16.0f; + const ImWchar* GlyphRanges = nullptr; + bool MergeWithLast = false; + }; + + class Fonts + { + public: + static void Add(const FontConfiguration& config, bool isDefault = false); + static void PushFont(const std::string& fontName); + static void PopFont(); + static ImFont* Get(const std::string& fontName); + }; + +} diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiLayer.cpp b/StarEngine/src/StarEngine/ImGui/ImGuiLayer.cpp index 3108051b..3b0ab58d 100644 --- a/StarEngine/src/StarEngine/ImGui/ImGuiLayer.cpp +++ b/StarEngine/src/StarEngine/ImGui/ImGuiLayer.cpp @@ -1,34 +1,38 @@ #include "sepch.h" -#include "StarEngine/ImGui/ImGuiLayer.h" +#include "ImGuiLayer.h" -#include "StarEngine/Core/Application.h" +#include "Colors.h" -#include -#include +#include "StarEngine/Core/Input.h" -#include -#include +#include "StarEngine/Renderer/Renderer.h" -// TEMPORARY -#include -#include +#include "StarEngine/Platform/Vulkan/VulkanImGuiLayer.h" +#include "StarEngine/Platform/Vulkan/VulkanSwapChain.h" +#include "StarEngine/Platform/Vulkan/VulkanDeviceManager.h" -#include "ImGuizmo.h" +#include "StarEngine/Renderer/RendererAPI.h" +#include "StarEngine/Editor/FontAwesome.h" +#include "StarEngine/ImGui/ImGuizmo.h" +#include "StarEngine/ImGui/ImGuiFonts.h" -namespace StarEngine { +#include - ImGuiLayer::ImGuiLayer() - : Layer("ImGuiLayer") - { +#include "backends/imgui_impl_glfw.h" - } +#include "ImGuiBuild.cpp" + + +// TODO(Yan): WIP +// Defined in imgui_impl_glfw.cpp +// extern bool g_DisableImGuiEvents; + +namespace StarEngine { void ImGuiLayer::OnAttach() { - SE_PROFILE_FUNCTION(); - // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -37,85 +41,177 @@ namespace StarEngine { //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows - //io.ConfigFlags |= ImGuiConfigFlags_ViewportsNoTaskBarIcons; - //io.ConfigFlags |= ImGuiConfigFlags_ViewportsNoMerge; - - float fontSize = 18.0f;// *2.0f; - io.Fonts->AddFontFromFileTTF("assets/fonts/opensans/OpenSans-Bold.ttf", fontSize); - io.FontDefault = io.Fonts->AddFontFromFileTTF("assets/fonts/opensans/OpenSans-Regular.ttf", fontSize); + // Configure Fonts + { + UI::FontConfiguration robotoBold; + robotoBold.FontName = "Bold"; + robotoBold.FilePath = "Resources/Fonts/Roboto/Roboto-Bold.ttf"; + robotoBold.Size = 18.0f; + UI::Fonts::Add(robotoBold); + + UI::FontConfiguration robotoLarge; + robotoLarge.FontName = "Large"; + robotoLarge.FilePath = "Resources/Fonts/Roboto/Roboto-Regular.ttf"; + robotoLarge.Size = 24.0f; + UI::Fonts::Add(robotoLarge); + + UI::FontConfiguration robotoDefault; + robotoDefault.FontName = "Default"; + robotoDefault.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoDefault.Size = 15.0f; + UI::Fonts::Add(robotoDefault, true); + + static const ImWchar s_FontAwesomeRanges[] = { SE_ICON_MIN, SE_ICON_MAX, 0 }; + UI::FontConfiguration fontAwesome; + fontAwesome.FontName = "FontAwesome"; + fontAwesome.FilePath = "Resources/Fonts/FontAwesome/fontawesome-webfont.ttf"; + fontAwesome.Size = 16.0f; + fontAwesome.GlyphRanges = s_FontAwesomeRanges; + fontAwesome.MergeWithLast = true; + UI::Fonts::Add(fontAwesome); + + UI::FontConfiguration robotoMedium; + robotoMedium.FontName = "Medium"; + robotoMedium.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoMedium.Size = 18.0f; + UI::Fonts::Add(robotoMedium); + + UI::FontConfiguration robotoSmall; + robotoSmall.FontName = "Small"; + robotoSmall.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoSmall.Size = 12.0f; + UI::Fonts::Add(robotoSmall); + + UI::FontConfiguration robotoExtraSmall; + robotoExtraSmall.FontName = "ExtraSmall"; + robotoExtraSmall.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoExtraSmall.Size = 10.0f; + UI::Fonts::Add(robotoExtraSmall); + + UI::FontConfiguration robotoBoldTitle; + robotoBoldTitle.FontName = "BoldTitle"; + robotoBoldTitle.FilePath = "Resources/Fonts/Roboto/Roboto-Bold.ttf"; + robotoBoldTitle.Size = 16.0f; + UI::Fonts::Add(robotoBoldTitle); + } // Setup Dear ImGui style ImGui::StyleColorsDark(); - //ImGui::StyleColorsClassic(); + SetDarkThemeV2Colors(); // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones. ImGuiStyle& style = ImGui::GetStyle(); - - style.ScaleAllSizes(Window::s_HighDPIScaleFactor); - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { style.WindowRounding = 0.0f; style.Colors[ImGuiCol_WindowBg].w = 1.0f; } + style.Colors[ImGuiCol_WindowBg] = ImVec4(0.15f, 0.15f, 0.15f, style.Colors[ImGuiCol_WindowBg].w); - SetDarkThemeColors(); + ImGui_ImplGlfw_InitForVulkan((GLFWwindow*)Application::Get().GetWindow().GetNativeWindow(), true); - Application& app = Application::Get(); - GLFWwindow* window = static_cast(app.GetWindow().GetNativeWindow()); + m_ImGuiRenderer = std::make_unique(); + m_ImGuiRenderer->Init(); - // Setup Platform/Renderer bindings - ImGui_ImplGlfw_InitForOpenGL(window, true); - ImGui_ImplOpenGL3_Init("#version 410"); + InitPlatformInterface(); } - void ImGuiLayer::OnDetach() + struct ImGuiViewportData { - SE_PROFILE_FUNCTION(); + bool WindowOwned = false; + std::unique_ptr SC; + std::unique_ptr Renderer; + }; - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); + static void ImGuiRenderer_CreateWindow(ImGuiViewport* viewport) + { + ImGuiViewportData* data = IM_NEW(ImGuiViewportData)(); + viewport->RendererUserData = data; + + vk::Instance vInstance = ((VulkanDeviceManager*)Application::GetGraphicsDeviceManager())->GetVulkanInstance(); + + // Create surface + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + vk::SurfaceKHR surface; + VkResult err = (VkResult)platform_io.Platform_CreateVkSurface(viewport, *(ImU64*)&vInstance, nullptr, (ImU64*)&surface); + SE_CORE_ASSERT(err == VkResult::VK_SUCCESS); + + data->SC = std::make_unique(surface); + data->SC->Create((uint32_t)viewport->Size.x, (uint32_t)viewport->Size.y); + data->WindowOwned = true; + + data->Renderer = std::make_unique(); + data->Renderer->Init(); } - void ImGuiLayer::OnEvent(Event& e) + static void ImGuiRenderer_DestroyWindow(ImGuiViewport* viewport) { - if (m_BlockEvents) { - ImGuiIO& io = ImGui::GetIO(); - e.Handled |= e.IsInCategory(EventCategoryMouse) & io.WantCaptureMouse; - e.Handled |= e.IsInCategory(EventCategoryKeyboard) & io.WantCaptureKeyboard; - } + ImGuiViewportData* vd = (ImGuiViewportData*)viewport->RendererUserData; + sdelete vd; + viewport->RendererUserData = nullptr; + } + + static void ImGuiRenderer_SetWindowSize(ImGuiViewport* viewport, ImVec2 size) + { + ImGuiViewportData* vd = (ImGuiViewportData*)viewport->RendererUserData; + vd->SC->OnResize((uint32_t)size.x, (uint32_t)size.y); + } + + static void ImGuiRenderer_RenderWindow(ImGuiViewport* viewport, void*) + { + ImGuiViewportData* vd = (ImGuiViewportData*)viewport->RendererUserData; + vd->SC->BeginFrame(); + vd->Renderer->UpdateFontTexture(); + vd->Renderer->RenderToSwapchain(viewport, vd->SC.get()); + } + + static void ImGuiRenderer_SwapBuffers(ImGuiViewport* viewport, void*) + { + ImGuiViewportData* vd = (ImGuiViewportData*)viewport->RendererUserData; + vd->SC->Present(); + } + + void ImGuiLayer::InitPlatformInterface() + { + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + IM_ASSERT(platform_io.Platform_CreateVkSurface != NULL && "Platform needs to setup the CreateVkSurface handler."); + + platform_io.Renderer_CreateWindow = ImGuiRenderer_CreateWindow; + platform_io.Renderer_DestroyWindow = ImGuiRenderer_DestroyWindow; + platform_io.Renderer_SetWindowSize = ImGuiRenderer_SetWindowSize; + platform_io.Renderer_RenderWindow = ImGuiRenderer_RenderWindow; + platform_io.Renderer_SwapBuffers = ImGuiRenderer_SwapBuffers; + } + + void ImGuiLayer::OnDetach() + { + ImGui::DestroyContext(); } void ImGuiLayer::Begin() { - SE_PROFILE_FUNCTION(); + ImGui::SetMouseCursor(Input::GetCursorMode() == CursorMode::Normal ? ImGuiMouseCursor_Arrow : ImGuiMouseCursor_None); - ImGui_ImplOpenGL3_NewFrame(); + m_ImGuiRenderer->UpdateFontTexture(); ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); ImGuizmo::BeginFrame(); } void ImGuiLayer::End() { - SE_PROFILE_FUNCTION(); - - ImGuiIO& io = ImGui::GetIO(); - Application& app = Application::Get(); - io.DisplaySize = ImVec2((float)app.GetWindow().GetWidth(), (float)app.GetWindow().GetHeight()); - - // Rendering ImGui::Render(); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + m_ImGuiRenderer->RenderToSwapchain(ImGui::GetMainViewport(), &Application::Get().GetWindow().GetSwapChain()); + + // Update and Render additional Platform Windows + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { - GLFWwindow* backup_current_context = glfwGetCurrentContext(); ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); - glfwMakeContextCurrent(backup_current_context); } } @@ -128,7 +224,7 @@ namespace StarEngine { colors[ImGuiCol_Header] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f }; colors[ImGuiCol_HeaderHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f }; colors[ImGuiCol_HeaderActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f }; - + // Buttons colors[ImGuiCol_Button] = ImVec4{ 0.2f, 0.205f, 0.21f, 1.0f }; colors[ImGuiCol_ButtonHovered] = ImVec4{ 0.3f, 0.305f, 0.31f, 1.0f }; @@ -150,11 +246,115 @@ namespace StarEngine { colors[ImGuiCol_TitleBg] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f }; colors[ImGuiCol_TitleBgActive] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f }; colors[ImGuiCol_TitleBgCollapsed] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f }; + + // Resize Grip + colors[ImGuiCol_ResizeGrip] = ImVec4(0.91f, 0.91f, 0.91f, 0.25f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.81f, 0.81f, 0.81f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.46f, 0.46f, 0.46f, 0.95f); + + // Scrollbar + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.0f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.0f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.0f); + + // Check Mark + colors[ImGuiCol_CheckMark] = ImVec4(0.94f, 0.94f, 0.94f, 1.0f); + + // Slider + colors[ImGuiCol_SliderGrab] = ImVec4(0.51f, 0.51f, 0.51f, 0.7f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.66f, 0.66f, 0.66f, 1.0f); + + } + + void ImGuiLayer::SetDarkThemeV2Colors() + { + auto& style = ImGui::GetStyle(); + auto& colors = ImGui::GetStyle().Colors; + + //======================================================== + /// Colours + + // Headers + colors[ImGuiCol_Header] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::groupHeader); + colors[ImGuiCol_HeaderHovered] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::groupHeader); + colors[ImGuiCol_HeaderActive] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::groupHeader); + + // Buttons + colors[ImGuiCol_Button] = ImColor(56, 56, 56, 200); + colors[ImGuiCol_ButtonHovered] = ImColor(70, 70, 70, 255); + colors[ImGuiCol_ButtonActive] = ImColor(56, 56, 56, 150); + + // Frame BG + colors[ImGuiCol_FrameBg] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::propertyField); + colors[ImGuiCol_FrameBgHovered] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::propertyField); + colors[ImGuiCol_FrameBgActive] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::propertyField); + + // Tabs + colors[ImGuiCol_Tab] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); + colors[ImGuiCol_TabHovered] = ImColor(255, 225, 135, 30); + colors[ImGuiCol_TabActive] = ImColor(255, 225, 135, 60); + colors[ImGuiCol_TabUnfocused] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); + colors[ImGuiCol_TabUnfocusedActive] = colors[ImGuiCol_TabHovered]; + + // Title + colors[ImGuiCol_TitleBg] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); + colors[ImGuiCol_TitleBgActive] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4{ 0.15f, 0.1505f, 0.151f, 1.0f }; + + // Resize Grip + colors[ImGuiCol_ResizeGrip] = ImVec4(0.91f, 0.91f, 0.91f, 0.25f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.81f, 0.81f, 0.81f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.46f, 0.46f, 0.46f, 0.95f); + + // Scrollbar + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.0f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.0f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.0f); + + // Check Mark + colors[ImGuiCol_CheckMark] = ImColor(200, 200, 200, 255); + + // Slider + colors[ImGuiCol_SliderGrab] = ImVec4(0.51f, 0.51f, 0.51f, 0.7f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.66f, 0.66f, 0.66f, 1.0f); + + // Text + colors[ImGuiCol_Text] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::text); + + // Checkbox + colors[ImGuiCol_CheckMark] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::text); + + // Separator + colors[ImGuiCol_Separator] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::backgroundDark); + colors[ImGuiCol_SeparatorActive] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::highlight); + colors[ImGuiCol_SeparatorHovered] = ImColor(39, 185, 242, 150); + + // Window Background + colors[ImGuiCol_WindowBg] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::titlebar); + colors[ImGuiCol_ChildBg] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::background); + colors[ImGuiCol_PopupBg] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::backgroundPopup); + colors[ImGuiCol_Border] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::backgroundDark); + + // Tables + colors[ImGuiCol_TableHeaderBg] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::groupHeader); + colors[ImGuiCol_TableBorderLight] = ImGui::ColorConvertU32ToFloat4(Colors::Theme::backgroundDark); + + // Menubar + colors[ImGuiCol_MenuBarBg] = ImVec4{ 0.0f, 0.0f, 0.0f, 0.0f }; + + //======================================================== + /// Style + style.FrameRounding = 2.5f; + style.FrameBorderSize = 1.0f; + style.IndentSpacing = 11.0f; } - uint32_t ImGuiLayer::GetActiveWidgetID() const + void ImGuiLayer::AllowInputEvents(bool allowEvents) { - return GImGui->ActiveId; + // TODO + // g_DisableImGuiEvents = !allowEvents; } } diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiLayer.h b/StarEngine/src/StarEngine/ImGui/ImGuiLayer.h index 2b9b4c98..3cb5757f 100644 --- a/StarEngine/src/StarEngine/ImGui/ImGuiLayer.h +++ b/StarEngine/src/StarEngine/ImGui/ImGuiLayer.h @@ -2,32 +2,34 @@ #include "StarEngine/Core/Layer.h" -#include "StarEngine/Events/ApplicationEvent.h" -#include "StarEngine/Events/KeyEvent.h" -#include "StarEngine/Events/MouseEvent.h" +#include "ImGuiRenderer.h" namespace StarEngine { class ImGuiLayer : public Layer { public: - ImGuiLayer(); - ~ImGuiLayer() = default; + static ImGuiLayer* Create() { return snew ImGuiLayer(); } virtual void OnAttach() override; virtual void OnDetach() override; - virtual void OnEvent(Event& e) override; void Begin(); void End(); - void BlockEvents(bool block) { m_BlockEvents = block; } - void SetDarkThemeColors(); + void SetDarkThemeV2Colors(); - uint32_t GetActiveWidgetID() const; + void AllowInputEvents(bool allowEvents); + public: + ImGuiLayer() = default; + virtual ~ImGuiLayer() = default; + private: + void InitPlatformInterface(); private: - bool m_BlockEvents = true; + std::unique_ptr m_ImGuiRenderer; }; + + } diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiRenderer.cpp b/StarEngine/src/StarEngine/ImGui/ImGuiRenderer.cpp new file mode 100644 index 00000000..151f780b --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiRenderer.cpp @@ -0,0 +1,449 @@ +/* +* Copyright (c) 2014-2021, NVIDIA CORPORATION. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +*/ + +/* +License for Dear ImGui + +Copyright (c) 2014-2019 Omar Cornut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "sepch.h" +#include "ImGuiRenderer.h" + +#include "StarEngine/Renderer/Shader.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Debug/Profiler.h" + +#include "StarEngine/Platform/Vulkan/VulkanSwapChain.h" + +#include "nvrhi/utils.h" + +#include + +namespace StarEngine { + + struct VERTEX_CONSTANT_BUFFER + { + float mvp[4][4]; + }; + + bool ImGuiRenderer::UpdateFontTexture() + { + nvrhi::IDevice* device = Application::GetGraphicsDevice(); + + ImGuiIO& io = ImGui::GetIO(); + + io.BackendRendererName = "StarEngineImGuiRenderer"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. + io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional) + // If the font texture exists and is bound to ImGui, we're done. + // Note: ImGui_Renderer will reset io.Fonts->TexID when new fonts are added. + if (m_FontTexture && io.Fonts->TexID) + return true; + + unsigned char* pixels; + int width, height; + + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + if (!pixels) + return false; + + nvrhi::TextureDesc textureDesc; + textureDesc.width = width; + textureDesc.height = height; + textureDesc.format = nvrhi::Format::RGBA8_UNORM; + textureDesc.debugName = "ImGui font texture"; + + m_FontTexture = device->createTexture(textureDesc); + + if (m_FontTexture == nullptr) + return false; + + m_RenderCommandBuffer->RT_Begin(); + nvrhi::CommandListHandle commandList = m_RenderCommandBuffer->GetActive(); + + commandList->beginTrackingTextureState(m_FontTexture, nvrhi::AllSubresources, nvrhi::ResourceStates::Common); + + commandList->writeTexture(m_FontTexture, 0, 0, pixels, width * 4); + + commandList->setPermanentTextureState(m_FontTexture, nvrhi::ResourceStates::ShaderResource); + commandList->commitBarriers(); + + m_RenderCommandBuffer->RT_End(); + m_RenderCommandBuffer->RT_Submit(); + + io.Fonts->TexID = (ImTextureID)m_FontTexture.Get(); + + return true; + } + + bool ImGuiRenderer::Init() + { + auto device = Application::GetGraphicsDevice(); + + m_RenderCommandBuffer = RenderCommandBuffer::Create(0, "ImGuiRenderer"); + + Ref imguiShader = Renderer::GetShaderLibrary()->Get("ImGui"); + m_VertexShader = imguiShader->GetHandle(nvrhi::ShaderType::Vertex); + m_PixelShader = imguiShader->GetHandle(nvrhi::ShaderType::Pixel); + + // create attribute layout object + nvrhi::VertexAttributeDesc vertexAttribLayout[] = { + { "POSITION", nvrhi::Format::RG32_FLOAT, 1, 0, offsetof(ImDrawVert,pos), sizeof(ImDrawVert), false }, + { "TEXCOORD", nvrhi::Format::RG32_FLOAT, 1, 0, offsetof(ImDrawVert,uv), sizeof(ImDrawVert), false }, + { "COLOR", nvrhi::Format::RGBA8_UNORM, 1, 0, offsetof(ImDrawVert,col), sizeof(ImDrawVert), false }, + }; + + m_ShaderAttribLayout = device->createInputLayout(vertexAttribLayout, sizeof(vertexAttribLayout) / sizeof(vertexAttribLayout[0]), m_VertexShader); + + // create PSO + { + nvrhi::BlendState blendState; + blendState.targets[0].setBlendEnable(true) + .setSrcBlend(nvrhi::BlendFactor::SrcAlpha) + .setDestBlend(nvrhi::BlendFactor::InvSrcAlpha) + .setSrcBlendAlpha(nvrhi::BlendFactor::One) + .setDestBlendAlpha(nvrhi::BlendFactor::InvSrcAlpha); + + auto rasterState = nvrhi::RasterState() + .setFillSolid() + .setCullNone() + .setScissorEnable(true) + .setDepthClipEnable(true); + + auto depthStencilState = nvrhi::DepthStencilState() + .disableDepthTest() + .enableDepthWrite() + .disableStencil() + .setDepthFunc(nvrhi::ComparisonFunc::Always); + + nvrhi::RenderState renderState; + renderState.blendState = blendState; + renderState.depthStencilState = depthStencilState; + renderState.rasterState = rasterState; + + nvrhi::BindingLayoutDesc layoutDesc; + layoutDesc.visibility = nvrhi::ShaderType::All; + layoutDesc.bindings = { + nvrhi::BindingLayoutItem::PushConstants(0, sizeof(glm::vec2) * 2), + nvrhi::BindingLayoutItem::Texture_SRV(0), + nvrhi::BindingLayoutItem::Sampler(1) + }; + m_BindingLayout = device->createBindingLayout(layoutDesc); + + m_BasePSODesc.primType = nvrhi::PrimitiveType::TriangleList; + m_BasePSODesc.inputLayout = m_ShaderAttribLayout; + m_BasePSODesc.VS = m_VertexShader; + m_BasePSODesc.PS = m_PixelShader; + m_BasePSODesc.renderState = renderState; + m_BasePSODesc.bindingLayouts = { m_BindingLayout }; + } + + { + const auto desc = nvrhi::SamplerDesc() + .setAllAddressModes(nvrhi::SamplerAddressMode::Wrap) + .setAllFilters(true); + + m_FontSampler = device->createSampler(desc); + + if (m_FontSampler == nullptr) + return false; + } + + return true; + } + + bool ImGuiRenderer::ReallocateBuffer(nvrhi::BufferHandle& buffer, size_t requiredSize, size_t reallocateSize, const bool indexBuffer) + { + nvrhi::IDevice* device = Application::GetGraphicsDevice(); + + if (buffer == nullptr || size_t(buffer->getDesc().byteSize) < requiredSize) + { + nvrhi::BufferDesc desc; + desc.byteSize = uint32_t(reallocateSize); + desc.structStride = 0; + desc.debugName = indexBuffer ? "ImGui index buffer" : "ImGui vertex buffer"; + desc.canHaveUAVs = false; + desc.isVertexBuffer = !indexBuffer; + desc.isIndexBuffer = indexBuffer; + desc.isDrawIndirectArgs = false; + desc.isVolatile = false; + desc.initialState = indexBuffer ? nvrhi::ResourceStates::IndexBuffer : nvrhi::ResourceStates::VertexBuffer; + desc.keepInitialState = true; + + buffer = device->createBuffer(desc); + + if (!buffer) + { + return false; + } + } + + return true; + } + + nvrhi::GraphicsPipelineHandle ImGuiRenderer::GetOrCreatePipeline(VulkanSwapChain* swapchain) + { + uint32_t currentFramebufferIndex = swapchain->GetCurrentBackBufferIndex(); + auto& swapchainPipelineCache = m_PipelineCache[swapchain]; + SE_CORE_VERIFY(currentFramebufferIndex < swapchainPipelineCache.Pipelines.max_size()); + + nvrhi::FramebufferHandle targetFramebuffer = swapchain->GetCurrentFramebuffer(); + + nvrhi::GraphicsPipelineHandle pipeline = swapchainPipelineCache.Pipelines[currentFramebufferIndex]; + bool invalidate = !pipeline || swapchainPipelineCache.Framebuffers[currentFramebufferIndex] != targetFramebuffer; + if (invalidate) + { + nvrhi::DeviceHandle device = Application::GetGraphicsDevice(); + pipeline = device->createGraphicsPipeline(m_BasePSODesc, targetFramebuffer); + swapchainPipelineCache.Pipelines[currentFramebufferIndex] = pipeline; + swapchainPipelineCache.Framebuffers[currentFramebufferIndex] = targetFramebuffer; + } + return pipeline; + } + + nvrhi::IBindingSet* ImGuiRenderer::GetBindingSet(nvrhi::ITexture* texture) + { + nvrhi::IDevice* device = Application::GetGraphicsDevice(); + + auto iter = m_BindingsCache.find(texture); + if (iter != m_BindingsCache.end()) + { + return iter->second; + } + + nvrhi::BindingSetDesc desc; + + desc.bindings = { + nvrhi::BindingSetItem::PushConstants(0, sizeof(float) * 2), + nvrhi::BindingSetItem::Texture_SRV(0, texture), + nvrhi::BindingSetItem::Sampler(1, m_FontSampler) + }; + + nvrhi::BindingSetHandle binding = device->createBindingSet(desc, m_BindingLayout); + SE_CORE_ASSERT(binding); + + m_BindingsCache[texture] = binding; + return binding; + } + + bool ImGuiRenderer::UpdateGeometry(ImDrawData* drawData) + { + nvrhi::CommandListHandle commandList = m_RenderCommandBuffer->GetActive(); + + // create/resize vertex and index buffers if needed + if (!ReallocateBuffer(m_VertexBuffer, + drawData->TotalVtxCount * sizeof(ImDrawVert), + (drawData->TotalVtxCount + 5000) * sizeof(ImDrawVert), + false)) + { + return false; + } + + if (!ReallocateBuffer(m_IndexBuffer, + drawData->TotalIdxCount * sizeof(ImDrawIdx), + (drawData->TotalIdxCount + 5000) * sizeof(ImDrawIdx), + true)) + { + return false; + } + + m_VertexBufferData.resize(m_VertexBuffer->getDesc().byteSize / sizeof(ImDrawVert)); + m_IndexBufferData.resize(m_IndexBuffer->getDesc().byteSize / sizeof(ImDrawIdx)); + + // copy and convert all vertices into a single contiguous buffer + ImDrawVert* vtxDst = &m_VertexBufferData[0]; + ImDrawIdx* idxDst = &m_IndexBufferData[0]; + + for (int n = 0; n < drawData->CmdListsCount; n++) + { + const ImDrawList* cmdList = drawData->CmdLists[n]; + + memcpy(vtxDst, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.Size * sizeof(ImDrawVert)); + memcpy(idxDst, cmdList->IdxBuffer.Data, cmdList->IdxBuffer.Size * sizeof(ImDrawIdx)); + + vtxDst += cmdList->VtxBuffer.Size; + idxDst += cmdList->IdxBuffer.Size; + } + + commandList->writeBuffer(m_VertexBuffer, &m_VertexBufferData[0], m_VertexBuffer->getDesc().byteSize); + commandList->writeBuffer(m_IndexBuffer, &m_IndexBufferData[0], m_IndexBuffer->getDesc().byteSize); + + return true; + } + + bool ImGuiRenderer::Render(ImGuiViewport* viewport, nvrhi::GraphicsPipelineHandle pipeline, nvrhi::FramebufferHandle framebuffer, vk::Semaphore waitSemaphore) + { + SE_PROFILE_FUNCTION("ImGuiRenderer::Render"); + + nvrhi::IDevice* device = Application::GetGraphicsDevice(); + + ImDrawData* drawData = viewport->DrawData; + + m_RenderCommandBuffer->RT_Begin(); + nvrhi::CommandListHandle commandList = m_RenderCommandBuffer->GetActive(); + + // std::string markerName = std::format("ImGui (Viewport {})", viewport == ImGui::GetMainViewport() ? "Main" : std::to_string((uint64_t)viewport)); + // m_CommandList->beginMarker(markerName.c_str()); + nvrhi::utils::ClearColorAttachment(m_RenderCommandBuffer->GetActive(), framebuffer, 0, nvrhi::Color(1, 0, 1, 1)); + + if (!UpdateGeometry(drawData)) + { + m_RenderCommandBuffer->End(); + return false; + } + + // handle DPI scaling + drawData->ScaleClipRects(drawData->FramebufferScale); + + struct PushConstants + { + glm::vec2 Scale; + glm::vec2 Translate; + } pushConstants; + + pushConstants.Scale.x = 2.0f / drawData->DisplaySize.x; + pushConstants.Scale.y = 2.0f / drawData->DisplaySize.y; + pushConstants.Translate.x = -1.0f - drawData->DisplayPos.x * pushConstants.Scale.x; + pushConstants.Translate.y = -1.0f - drawData->DisplayPos.y * pushConstants.Scale.y; + + float fbWidth = drawData->DisplaySize.x * drawData->FramebufferScale.x; + float fbHeight = drawData->DisplaySize.y * drawData->FramebufferScale.y; + + // set up graphics state + nvrhi::GraphicsState drawState; + + drawState.framebuffer = framebuffer; + SE_CORE_ASSERT(drawState.framebuffer); + + drawState.pipeline = pipeline; + + drawState.viewport.viewports.push_back(nvrhi::Viewport(fbWidth, fbHeight)); + drawState.viewport.scissorRects.resize(1); // updated below + + nvrhi::VertexBufferBinding vbufBinding; + vbufBinding.buffer = m_VertexBuffer; + vbufBinding.slot = 0; + vbufBinding.offset = 0; + drawState.vertexBuffers.push_back(vbufBinding); + + drawState.indexBuffer.buffer = m_IndexBuffer; + drawState.indexBuffer.format = (sizeof(ImDrawIdx) == 2 ? nvrhi::Format::R16_UINT : nvrhi::Format::R32_UINT); + drawState.indexBuffer.offset = 0; + + // Will project scissor/clipping rectangles into framebuffer space + ImVec2 clip_off = drawData->DisplayPos; // (0,0) unless using multi-viewports + ImVec2 clip_scale = drawData->FramebufferScale; // (1,1) unless using retina display which are often (2,2) + + // render command lists + int vtxOffset = 0; + int idxOffset = 0; + for (int n = 0; n < drawData->CmdListsCount; n++) + { + const ImDrawList* cmdList = drawData->CmdLists[n]; + for (int i = 0; i < cmdList->CmdBuffer.Size; i++) + { + const ImDrawCmd* pCmd = &cmdList->CmdBuffer[i]; + + if (pCmd->UserCallback) + { + pCmd->UserCallback(cmdList, pCmd); + } + else + { + drawState.bindings = { GetBindingSet((nvrhi::ITexture*)pCmd->TextureId) }; + SE_CORE_ASSERT(drawState.bindings[0]); + + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clipMin((pCmd->ClipRect.x - clip_off.x) * clip_scale.x, (pCmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clipMax((pCmd->ClipRect.z - clip_off.x) * clip_scale.x, (pCmd->ClipRect.w - clip_off.y) * clip_scale.y); + + // Clamp to viewport as vkCmdSetScissor() won't accept values that are off bounds + if (clipMin.x < 0.0f) + clipMin.x = 0.0f; + if (clipMin.y < 0.0f) + clipMin.y = 0.0f; + if (clipMax.x > fbWidth) + clipMax.x = (float)fbWidth; + if (clipMax.y > fbHeight) + clipMax.y = (float)fbHeight; + if (clipMax.x <= clipMin.x || clipMax.y <= clipMin.y) + continue; + + drawState.viewport.scissorRects[0] = nvrhi::Rect(clipMin.x, clipMax.x, clipMin.y, clipMax.y); + + nvrhi::DrawArguments drawArguments; + drawArguments.vertexCount = pCmd->ElemCount; + drawArguments.startIndexLocation = pCmd->IdxOffset + idxOffset; + drawArguments.startVertexLocation = pCmd->VtxOffset + vtxOffset; + + m_RenderCommandBuffer->GetActive()->setGraphicsState(drawState); + commandList->setPushConstants(&pushConstants, sizeof(PushConstants)); + commandList->drawIndexed(drawArguments); + } + + } + + vtxOffset += cmdList->VtxBuffer.Size; + idxOffset += cmdList->IdxBuffer.Size; + } + + m_RenderCommandBuffer->RT_End(); + + // Wait for image to be available before rendering to it + if (waitSemaphore) + m_RenderCommandBuffer->RT_Wait(waitSemaphore); + + + m_RenderCommandBuffer->RT_Submit(); + + return true; + } + + bool ImGuiRenderer::RenderToSwapchain(ImGuiViewport* viewport, VulkanSwapChain* swapchain) + { + return Render(viewport, GetOrCreatePipeline(swapchain), swapchain->GetCurrentFramebuffer(), swapchain->GetAcquiredImageSemaphore()); + } + + void ImGuiRenderer::BackbufferResizing() + { + m_PipelineCache.clear(); + } + +} diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiRenderer.h b/StarEngine/src/StarEngine/ImGui/ImGuiRenderer.h new file mode 100644 index 00000000..e7ba295d --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiRenderer.h @@ -0,0 +1,112 @@ +/* +* Copyright (c) 2014-2021, NVIDIA CORPORATION. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +*/ + +/* +License for Dear ImGui + +Copyright (c) 2014-2019 Omar Cornut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "nvrhi/nvrhi.h" + +#include + +#include "StarEngine/Renderer/RenderCommandBuffer.h" + +namespace StarEngine +{ + class VulkanSwapChain; + + class ImGuiRenderer + { + public: + ImGuiRenderer() = default; + + bool Init(); + bool UpdateFontTexture(); + bool Render(ImGuiViewport* viewport, nvrhi::GraphicsPipelineHandle pipeline, nvrhi::FramebufferHandle framebuffer, vk::Semaphore waitSemaphore = nullptr); + bool RenderToSwapchain(ImGuiViewport* viewport, VulkanSwapChain* swapchain); + void BackbufferResizing(); + private: + bool ReallocateBuffer(nvrhi::BufferHandle& buffer, size_t requiredSize, size_t reallocateSize, bool isIndexBuffer); + + nvrhi::GraphicsPipelineHandle GetOrCreatePipeline(VulkanSwapChain* swapchain); + + nvrhi::IBindingSet* GetBindingSet(nvrhi::ITexture* texture); + bool UpdateGeometry(ImDrawData* drawData); + private: + Ref m_RenderCommandBuffer; + + nvrhi::ShaderHandle m_VertexShader; + nvrhi::ShaderHandle m_PixelShader; + nvrhi::InputLayoutHandle m_ShaderAttribLayout; + + nvrhi::TextureHandle m_FontTexture; + nvrhi::SamplerHandle m_FontSampler; + + nvrhi::BufferHandle m_VertexBuffer; + nvrhi::BufferHandle m_IndexBuffer; + + nvrhi::BindingLayoutHandle m_BindingLayout; + nvrhi::GraphicsPipelineDesc m_BasePSODesc; + + std::unordered_map m_BindingsCache; + + std::vector m_VertexBufferData; + std::vector m_IndexBufferData; + + + struct SwapchainPipelineCache + { + std::array Framebuffers; + std::array Pipelines; + }; + + std::map m_PipelineCache; + + }; +} diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiUtilities.cpp b/StarEngine/src/StarEngine/ImGui/ImGuiUtilities.cpp new file mode 100644 index 00000000..eca14c85 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiUtilities.cpp @@ -0,0 +1,709 @@ +#include +#include "ImGuiUtilities.h" +#include "ImGui.h" + +#include "StarEngine/Renderer/RendererAPI.h" + +#include "StarEngine/ImGui/UICore.h" + +namespace StarEngine::UI { + + ScopedDisable::ScopedDisable(bool disabled /*= true*/) + { + UI::BeginDisabled(disabled); + } + + ScopedDisable::~ScopedDisable() + { + UI::EndDisabled(); + } + + bool BeginPopup(const char* str_id, ImGuiWindowFlags flags) + { + bool opened = false; + if (ImGui::BeginPopup(str_id, flags)) + { + opened = true; + // Fill background wiht nice gradient + const float padding = ImGui::GetStyle().WindowBorderSize; + const ImRect windowRect = UI::RectExpanded(ImGui::GetCurrentWindow()->Rect(), -padding, -padding); + ImGui::PushClipRect(windowRect.Min, windowRect.Max, false); + const ImColor col1 = ImGui::GetStyleColorVec4(ImGuiCol_PopupBg);// Colors::Theme::backgroundPopup; + const ImColor col2 = UI::ColourWithMultipliedValue(col1, 0.8f); + ImGui::GetWindowDrawList()->AddRectFilledMultiColor(windowRect.Min, windowRect.Max, col1, col1, col2, col2); + ImGui::GetWindowDrawList()->AddRect(windowRect.Min, windowRect.Max, UI::ColourWithMultipliedValue(col1, 1.1f)); + ImGui::PopClipRect(); + + // Popped in EndPopup() + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(0, 0, 0, 80)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(1.0f, 1.0f)); + } + + return opened; + } + + void EndPopup() + { + ImGui::PopStyleVar(); // WindowPadding; + ImGui::PopStyleColor(); // HeaderHovered; + ImGui::EndPopup(); + } + + // MenuBar which allows you to specify its rectangle + bool BeginMenuBar(const ImRect& barRectangle) + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + /*if (!(window->Flags & ImGuiWindowFlags_MenuBar)) + return false;*/ + + IM_ASSERT(!window->DC.MenuBarAppending); + ImGui::BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore + ImGui::PushID("##menubar"); + + const ImVec2 padding = window->WindowPadding; + + // We don't clip with current window clipping rectangle as it is already set to the area below. However we clip with window full rect. + // We remove 1 worth of rounding to Max.x to that text in long menus and small windows don't tend to display over the lower-right rounded area, which looks particularly glitchy. + ImRect bar_rect = UI::RectOffset(barRectangle, 0.0f, padding.y);// window->MenuBarRect(); + ImRect clip_rect(IM_ROUND(ImMax(window->Pos.x, bar_rect.Min.x + window->WindowBorderSize + window->Pos.x - 10.0f)), IM_ROUND(bar_rect.Min.y + window->WindowBorderSize + window->Pos.y), + IM_ROUND(ImMax(bar_rect.Min.x + window->Pos.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize))), IM_ROUND(bar_rect.Max.y + window->Pos.y)); + + clip_rect.ClipWith(window->OuterRectClipped); + ImGui::PushClipRect(clip_rect.Min, clip_rect.Max, false); + + // We overwrite CursorMaxPos because BeginGroup sets it to CursorPos (essentially the .EmitItem hack in EndMenuBar() would need something analogous here, maybe a BeginGroupEx() with flags). + window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->Pos.x, bar_rect.Min.y + window->Pos.y); + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + window->DC.MenuBarAppending = true; + ImGui::AlignTextToFramePadding(); + return true; + } + + void EndMenuBar() + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. + if (ImGui::NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + { + // Try to find out if the request is for one of our child menu + ImGuiWindow* nav_earliest_child = g.NavWindow; + while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) + nav_earliest_child = nav_earliest_child->ParentWindow; + if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) + { + // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. + // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth bothering) + const ImGuiNavLayer layer = ImGuiNavLayer_Menu; + IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check + ImGui::FocusWindow(window); + ImGui::SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + g.NavCursorVisible = false; // Hide highlight for the current frame so we don't see the intermediary selection. + g.NavHighlightItemUnderNav = g.NavMousePosDirty = true; + ImGui::NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat + } + } + + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" + // IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); // NOTE(Yan): Needs to be commented out because Jay + IM_ASSERT(window->DC.MenuBarAppending); + ImGui::PopClipRect(); + ImGui::PopID(); + window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. + g.GroupStack.back().EmitItem = false; + ImGui::EndGroup(); // Restore position on layer 0 + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + window->DC.MenuBarAppending = false; + } + + // Exposed to be used for window with disabled decorations + // This border is going to be drawn even if window border size is set to 0.0f + void RenderWindowOuterBorders(ImGuiWindow* window) + { + struct ImGuiResizeBorderDef + { + ImVec2 InnerDir; + ImVec2 SegmentN1, SegmentN2; + float OuterAngle; + }; + + static const ImGuiResizeBorderDef resize_border_def[4] = + { + { ImVec2(+1, 0), ImVec2(0, 1), ImVec2(0, 0), IM_PI * 1.00f }, // Left + { ImVec2(-1, 0), ImVec2(1, 0), ImVec2(1, 1), IM_PI * 0.00f }, // Right + { ImVec2(0, +1), ImVec2(0, 0), ImVec2(1, 0), IM_PI * 1.50f }, // Up + { ImVec2(0, -1), ImVec2(1, 1), ImVec2(0, 1), IM_PI * 0.50f } // Down + }; + + auto GetResizeBorderRect = [](ImGuiWindow* window, int border_n, float perp_padding, float thickness) + { + ImRect rect = window->Rect(); + if (thickness == 0.0f) + { + rect.Max.x -= 1; + rect.Max.y -= 1; + } + if (border_n == ImGuiDir_Left) { return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Right) { return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Up) { return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness); } + if (border_n == ImGuiDir_Down) { return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness); } + IM_ASSERT(0); + return ImRect(); + }; + + + ImGuiContext& g = *GImGui; + float rounding = window->WindowRounding; + float border_size = 1.0f; // window->WindowBorderSize; + if (border_size > 0.0f && !(window->Flags & ImGuiWindowFlags_NoBackground)) + window->DrawList->AddRect(window->Pos, { window->Pos.x + window->Size.x, window->Pos.y + window->Size.y }, ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + + int border_held = window->ResizeBorderHeld; + if (border_held != -1) + { + const ImGuiResizeBorderDef& def = resize_border_def[border_held]; + ImRect border_r = GetResizeBorderRect(window, border_held, rounding, 0.0f); + ImVec2 p1 = ImLerp(border_r.Min, border_r.Max, def.SegmentN1); + const float offsetX = def.InnerDir.x * rounding; + const float offsetY = def.InnerDir.y * rounding; + p1.x += 0.5f + offsetX; + p1.y += 0.5f + offsetY; + + ImVec2 p2 = ImLerp(border_r.Min, border_r.Max, def.SegmentN2); + p2.x += 0.5f + offsetX; + p2.y += 0.5f + offsetY; + + window->DrawList->PathArcTo(p1, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); + window->DrawList->PathArcTo(p2, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); + window->DrawList->PathStroke(ImGui::GetColorU32(ImGuiCol_SeparatorActive), 0, ImMax(2.0f, border_size)); // Thicker than usual + } + if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) + { + float y = window->Pos.y + window->TitleBarHeight - 1; + window->DrawList->AddLine(ImVec2(window->Pos.x + border_size, y), ImVec2(window->Pos.x + window->Size.x - border_size, y), ImGui::GetColorU32(ImGuiCol_Border), g.Style.FrameBorderSize); + } + } + + // Exposed resize behavior for native OS windows + bool UpdateWindowManualResize(ImGuiWindow* window, ImVec2& newSize, ImVec2& newPosition) + { + + auto CalcWindowSizeAfterConstraint = [](ImGuiWindow* window, const ImVec2& size_desired) + { + ImGuiContext& g = *GImGui; + ImVec2 new_size = size_desired; + if (g.NextWindowData.WindowFlags & ImGuiNextWindowDataFlags_HasSizeConstraint) + { + // Using -1,-1 on either X/Y axis to preserve the current size. + ImRect cr = g.NextWindowData.SizeConstraintRect; + new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; + new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; + if (g.NextWindowData.SizeCallback) + { + ImGuiSizeCallbackData data; + data.UserData = g.NextWindowData.SizeCallbackUserData; + data.Pos = window->Pos; + data.CurrentSize = window->SizeFull; + data.DesiredSize = new_size; + g.NextWindowData.SizeCallback(&data); + new_size = data.DesiredSize; + } + new_size.x = IM_FLOOR(new_size.x); + new_size.y = IM_FLOOR(new_size.y); + } + + // Minimum size + if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) + { + ImGuiWindow* window_for_height = (window->DockNodeAsHost && window->DockNodeAsHost->VisibleWindow) ? window->DockNodeAsHost->VisibleWindow : window; + const float decoration_up_height = window_for_height->TitleBarHeight + window_for_height->MenuBarHeight; + new_size = ImMax(new_size, g.Style.WindowMinSize); + new_size.y = ImMax(new_size.y, decoration_up_height + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); // Reduce artifacts with very small windows + } + return new_size; + }; + + auto CalcWindowAutoFitSize = [CalcWindowSizeAfterConstraint](ImGuiWindow* window, const ImVec2& size_contents) + { + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + const float decoration_up_height = window->TitleBarHeight + window->MenuBarHeight; + ImVec2 size_pad{ window->WindowPadding.x * 2.0f, window->WindowPadding.y * 2.0f }; + ImVec2 size_desired = { size_contents.x + size_pad.x + 0.0f, size_contents.y + size_pad.y + decoration_up_height }; + if (window->Flags & ImGuiWindowFlags_Tooltip) + { + // Tooltip always resize + return size_desired; + } + else + { + // Maximum window size is determined by the viewport size or monitor size + const bool is_popup = (window->Flags & ImGuiWindowFlags_Popup) != 0; + const bool is_menu = (window->Flags & ImGuiWindowFlags_ChildMenu) != 0; + ImVec2 size_min = style.WindowMinSize; + if (is_popup || is_menu) // Popups and menus bypass style.WindowMinSize by default, but we give then a non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) + size_min = ImMin(size_min, ImVec2(4.0f, 4.0f)); + + // FIXME-VIEWPORT-WORKAREA: May want to use GetWorkSize() instead of Size depending on the type of windows? + ImVec2 avail_size = window->Viewport->Size; + if (window->ViewportOwned) + avail_size = ImVec2(FLT_MAX, FLT_MAX); + const int monitor_idx = window->ViewportAllowPlatformMonitorExtend; + if (monitor_idx >= 0 && monitor_idx < g.PlatformIO.Monitors.Size) + avail_size = g.PlatformIO.Monitors[monitor_idx].WorkSize; + ImVec2 size_auto_fit = ImClamp(size_desired, size_min, ImMax(size_min, { avail_size.x - style.DisplaySafeAreaPadding.x * 2.0f, + avail_size.y - style.DisplaySafeAreaPadding.y * 2.0f })); + + // When the window cannot fit all contents (either because of constraints, either because screen is too small), + // we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding. + ImVec2 size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit); + bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x - size_pad.x - 0.0f < size_contents.x && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar); + bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y - size_pad.y - decoration_up_height < size_contents.y && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar); + if (will_have_scrollbar_x) + size_auto_fit.y += style.ScrollbarSize; + if (will_have_scrollbar_y) + size_auto_fit.x += style.ScrollbarSize; + return size_auto_fit; + } + }; + + ImGuiContext& g = *GImGui; + + // Decide if we are going to handle borders and resize grips + const bool handle_borders_and_resize_grips = (window->DockNodeAsHost || !window->DockIsActive); + + if (!handle_borders_and_resize_grips || window->Collapsed) + return false; + + const ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, window->ContentSizeIdeal); + + // Handle manual resize: Resize Grips, Borders, Gamepad + int border_held = -1; + ImU32 resize_grip_col[4] = {}; + const int resize_grip_count = g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it. + const float resize_grip_draw_size = IM_FLOOR(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + window->ResizeBorderHeld = (signed char)border_held; + + //const ImRect& visibility_rect; + + struct ImGuiResizeBorderDef + { + ImVec2 InnerDir; + ImVec2 SegmentN1, SegmentN2; + float OuterAngle; + }; + static const ImGuiResizeBorderDef resize_border_def[4] = + { + { ImVec2(+1, 0), ImVec2(0, 1), ImVec2(0, 0), IM_PI * 1.00f }, // Left + { ImVec2(-1, 0), ImVec2(1, 0), ImVec2(1, 1), IM_PI * 0.00f }, // Right + { ImVec2(0, +1), ImVec2(0, 0), ImVec2(1, 0), IM_PI * 1.50f }, // Up + { ImVec2(0, -1), ImVec2(1, 1), ImVec2(0, 1), IM_PI * 0.50f } // Down + }; + + // Data for resizing from corner + struct ImGuiResizeGripDef + { + ImVec2 CornerPosN; + ImVec2 InnerDir; + int AngleMin12, AngleMax12; + }; + static const ImGuiResizeGripDef resize_grip_def[4] = + { + { ImVec2(1, 1), ImVec2(-1, -1), 0, 3 }, // Lower-right + { ImVec2(0, 1), ImVec2(+1, -1), 3, 6 }, // Lower-left + { ImVec2(0, 0), ImVec2(+1, +1), 6, 9 }, // Upper-left (Unused) + { ImVec2(1, 0), ImVec2(-1, +1), 9, 12 } // Upper-right (Unused) + }; + + auto CalcResizePosSizeFromAnyCorner = [CalcWindowSizeAfterConstraint](ImGuiWindow* window, const ImVec2& corner_target, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size) + { + ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left + ImVec2 pos_max = ImLerp({ window->Pos.x + window->Size.x, window->Pos.y + window->Size.y }, corner_target, corner_norm); // Expected window lower-right + ImVec2 size_expected = { pos_max.x - pos_min.x, pos_max.y - pos_min.y }; + ImVec2 size_constrained = CalcWindowSizeAfterConstraint(window, size_expected); + *out_pos = pos_min; + if (corner_norm.x == 0.0f) + out_pos->x -= (size_constrained.x - size_expected.x); + if (corner_norm.y == 0.0f) + out_pos->y -= (size_constrained.y - size_expected.y); + *out_size = size_constrained; + }; + + auto GetResizeBorderRect = [](ImGuiWindow* window, int border_n, float perp_padding, float thickness) + { + ImRect rect = window->Rect(); + if (thickness == 0.0f) + { + rect.Max.x -= 1; + rect.Max.y -= 1; + } + if (border_n == ImGuiDir_Left) { return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Right) { return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Up) { return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness); } + if (border_n == ImGuiDir_Down) { return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness); } + IM_ASSERT(0); + return ImRect(); + }; + + static const float WINDOWS_HOVER_PADDING = 4.0f; // Extend outside window for hovering/resizing (maxxed with TouchPadding) and inside windows for borders. Affect FindHoveredWindow(). + static const float WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04f; // Reduce visual noise by only highlighting the border after a certain time. + + auto& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + + if (/*(flags & ImGuiWindowFlags_NoResize) || */(flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + return false; + if (window->WasActive == false) // Early out to avoid running this code for e.g. an hidden implicit/fallback Debug window. + return false; + + bool ret_auto_fit = false; + const int resize_border_count = g.IO.ConfigWindowsResizeFromEdges ? 4 : 0; + const float grip_draw_size = IM_FLOOR(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + const float grip_hover_inner_size = IM_FLOOR(grip_draw_size * 0.75f); + const float grip_hover_outer_size = g.IO.ConfigWindowsResizeFromEdges ? WINDOWS_HOVER_PADDING : 0.0f; + + ImVec2 pos_target(FLT_MAX, FLT_MAX); + ImVec2 size_target(FLT_MAX, FLT_MAX); + + // Calculate the range of allowed position for that window (to be movable and visible past safe area padding) + // When clamping to stay visible, we will enforce that window->Pos stays inside of visibility_rect. + ImRect viewport_rect(window->Viewport->GetMainRect()); + ImRect viewport_work_rect(window->Viewport->GetWorkRect()); + ImVec2 visibility_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); + ImRect visibility_rect({ viewport_work_rect.Min.x + visibility_padding.x, viewport_work_rect.Min.y + visibility_padding.y }, + { viewport_work_rect.Max.x - visibility_padding.x, viewport_work_rect.Max.y - visibility_padding.y }); + + // Clip mouse interaction rectangles within the viewport rectangle (in practice the narrowing is going to happen most of the time). + // - Not narrowing would mostly benefit the situation where OS windows _without_ decoration have a threshold for hovering when outside their limits. + // This is however not the case with current backends under Win32, but a custom borderless window implementation would benefit from it. + // - When decoration are enabled we typically benefit from that distance, but then our resize elements would be conflicting with OS resize elements, so we also narrow. + // - Note that we are unable to tell if the platform setup allows hovering with a distance threshold (on Win32, decorated window have such threshold). + // We only clip interaction so we overwrite window->ClipRect, cannot call PushClipRect() yet as DrawList is not yet setup. + const bool clip_with_viewport_rect = !(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) || (g.IO.MouseHoveredViewport != window->ViewportId) || !(window->Viewport->Flags & ImGuiViewportFlags_NoDecoration); + if (clip_with_viewport_rect) + window->ClipRect = window->Viewport->GetMainRect(); + + // Resize grips and borders are on layer 1 + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + + // Manual resize grips + ImGui::PushID("#RESIZE"); + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& def = resize_grip_def[resize_grip_n]; + + const ImVec2 corner = ImLerp(window->Pos, { window->Pos.x + window->Size.x, window->Pos.y + window->Size.y }, def.CornerPosN); + + // Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window + bool hovered, held; + const ImVec2 min = { corner.x - def.InnerDir.x * grip_hover_outer_size, corner.y - def.InnerDir.y * grip_hover_outer_size }; + const ImVec2 max = { corner.x + def.InnerDir.x * grip_hover_outer_size, corner.y + def.InnerDir.y * grip_hover_outer_size }; + ImRect resize_rect(min, max); + + if (resize_rect.Min.x > resize_rect.Max.x) ImSwap(resize_rect.Min.x, resize_rect.Max.x); + if (resize_rect.Min.y > resize_rect.Max.y) ImSwap(resize_rect.Min.y, resize_rect.Max.y); + ImGuiID resize_grip_id = window->GetID(resize_grip_n); // == GetWindowResizeCornerID() + ImGui::ButtonBehavior(resize_rect, resize_grip_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawList(window)->AddRect(resize_rect.Min, resize_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered || held) + g.MouseCursor = (resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE; + + if (held && g.IO.MouseDoubleClicked[0] && resize_grip_n == 0) + { + // Manual auto-fit when double-clicking + size_target = CalcWindowSizeAfterConstraint(window, size_auto_fit); + ret_auto_fit = true; + ImGui::ClearActiveID(); + } + else if (held) + { + // Resize from any of the four corners + // We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position + ImVec2 clamp_min = ImVec2(def.CornerPosN.x == 1.0f ? visibility_rect.Min.x : -FLT_MAX, def.CornerPosN.y == 1.0f ? visibility_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max = ImVec2(def.CornerPosN.x == 0.0f ? visibility_rect.Max.x : +FLT_MAX, def.CornerPosN.y == 0.0f ? visibility_rect.Max.y : +FLT_MAX); + + const float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImLerp(def.InnerDir.x * grip_hover_outer_size, def.InnerDir.x * -grip_hover_inner_size, def.CornerPosN.x); + const float y = g.IO.MousePos.y - g.ActiveIdClickOffset.y + ImLerp(def.InnerDir.y * grip_hover_outer_size, def.InnerDir.y * -grip_hover_inner_size, def.CornerPosN.y); + + ImVec2 corner_target(x, y); // Corner of the window corresponding to our corner grip + corner_target = ImClamp(corner_target, clamp_min, clamp_max); + CalcResizePosSizeFromAnyCorner(window, corner_target, def.CornerPosN, &pos_target, &size_target); + } + + // Only lower-left grip is visible before hovering/activating + if (resize_grip_n == 0 || held || hovered) + resize_grip_col[resize_grip_n] = ImGui::GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip); + } + for (int border_n = 0; border_n < resize_border_count; border_n++) + { + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const ImGuiAxis axis = (border_n == ImGuiDir_Left || border_n == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + + bool hovered, held; + ImRect border_rect = GetResizeBorderRect(window, border_n, grip_hover_inner_size, WINDOWS_HOVER_PADDING); + ImGuiID border_id = window->GetID(border_n + 4); // == GetWindowResizeBorderID() + ImGui::ButtonBehavior(border_rect, border_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren); + //GetForegroundDrawLists(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); + if ((hovered && g.HoveredIdTimer > WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) || held) + { + g.MouseCursor = (axis == ImGuiAxis_X) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS; + if (held) + border_held = border_n; + } + if (held) + { + ImVec2 clamp_min(border_n == ImGuiDir_Right ? visibility_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down ? visibility_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max(border_n == ImGuiDir_Left ? visibility_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? visibility_rect.Max.y : +FLT_MAX); + ImVec2 border_target = window->Pos; + border_target[axis] = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + WINDOWS_HOVER_PADDING; + border_target = ImClamp(border_target, clamp_min, clamp_max); + CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); + } + } + ImGui::PopID(); + + bool changed = false; + newSize = window->Size; + newPosition = window->Pos; + + // Apply back modified position/size to window + if (size_target.x != FLT_MAX) + { + //window->SizeFull = size_target; + //MarkIniSettingsDirty(window); + newSize = size_target; + changed = true; + } + if (pos_target.x != FLT_MAX) + { + //window->Pos = ImFloor(pos_target); + //MarkIniSettingsDirty(window); + newPosition = pos_target; + changed = true; + } + + //window->Size = window->SizeFull; + return changed; + } + + bool ContextMenuHeader(const char* label, ImGuiTreeNodeFlags flags) + { + bool opened = false; + if (ImGui::CollapsingHeader(label, flags)) + { + opened = true; + + auto* window = ImGui::GetCurrentWindow(); + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; + const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y)); + + const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapser arrow width + Spacing + const ImVec2 label_size = ImGui::CalcTextSize(label); + + ImGui::SameLine(); + ImGui::SetCursorPosX(0.0f); + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImVec2 start = ImVec2(text_offset_x + label_size.x + padding.x * 4.0f, ImGui::GetFrameHeight() * 0.5f) + pos; + const ImVec2 end = pos + ImVec2(ImGui::GetWindowWidth() - padding.x - window->ScrollbarSizes.x, ImGui::GetFrameHeight() * 0.5f); + window->DrawList->AddLine(start, end, ImGui::GetColorU32(ImGuiCol_Separator)); + + // next line + ImGui::Dummy(ImVec2(0.0f, ImGui::GetFrameHeight())); + } + return opened; + } + + //========================================================================================= + /// Shadows + + void DrawShadow(const Ref& shadowImage, int radius, ImVec2 rectMin, ImVec2 rectMax, float alphMultiplier, float lengthStretch, + bool drawLeft, bool drawRight, bool drawTop, bool drawBottom) + { + const float widthOffset = lengthStretch; + const float alphaTop = std::min(0.25f * alphMultiplier, 1.0f); + const float alphaSides = std::min(0.30f * alphMultiplier, 1.0f); + const float alphaBottom = std::min(0.60f * alphMultiplier, 1.0f); + const auto p1 = rectMin; + const auto p2 = rectMax; + + ImTextureID textureID = GetTextureID(shadowImage); + + auto* drawList = ImGui::GetWindowDrawList(); + if (drawLeft) + drawList->AddImage(textureID, { p1.x - widthOffset, p1.y - radius }, { p2.x + widthOffset, p1.y }, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), ImColor(0.0f, 0.0f, 0.0f, alphaTop)); + if (drawRight) + drawList->AddImage(textureID, { p1.x - widthOffset, p2.y }, { p2.x + widthOffset, p2.y + radius }, ImVec2(0.0f, 1.0f), ImVec2(1.0f, 0.0f), ImColor(0.0f, 0.0f, 0.0f, alphaBottom)); + + if (drawTop) + drawList->AddImageQuad(textureID, { p1.x - radius, p1.y - widthOffset }, { p1.x, p1.y - widthOffset }, { p1.x, p2.y + widthOffset }, { p1.x - radius, p2.y + widthOffset }, + { 0.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, ImColor(0.0f, 0.0f, 0.0f, alphaSides)); + if (drawBottom) + drawList->AddImageQuad(textureID, { p2.x, p1.y - widthOffset }, { p2.x + radius, p1.y - widthOffset }, { p2.x + radius, p2.y + widthOffset }, { p2.x, p2.y + widthOffset }, + { 0.0f, 1.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, ImColor(0.0f, 0.0f, 0.0f, alphaSides)); + }; + + void DrawShadow(const Ref& shadowImage, int radius, ImRect rectangle, float alphMultiplier, float lengthStretch, + bool drawLeft, bool drawRight, bool drawTop, bool drawBottom) + { + DrawShadow(shadowImage, radius, rectangle.Min, rectangle.Max, alphMultiplier, lengthStretch, drawLeft, drawRight, drawTop, drawBottom); + }; + + + void DrawShadow(const Ref& shadowImage, int radius, float alphMultiplier, float lengthStretch, + bool drawLeft, bool drawRight, bool drawTop, bool drawBottom) + { + DrawShadow(shadowImage, radius, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), alphMultiplier, lengthStretch, drawLeft, drawRight, drawTop, drawBottom); + }; + + void DrawShadowInner(const Ref& shadowImage, int radius, ImVec2 rectMin, ImVec2 rectMax, float alpha, float lengthStretch, + bool drawLeft, bool drawRight, bool drawTop, bool drawBottom) + { + const float widthOffset = lengthStretch; + const float alphaTop = alpha; //std::min(0.25f * alphMultiplier, 1.0f); + const float alphaSides = alpha; //std::min(0.30f * alphMultiplier, 1.0f); + const float alphaBottom = alpha; //std::min(0.60f * alphMultiplier, 1.0f); + const auto p1 = ImVec2(rectMin.x + radius, rectMin.y + radius); + const auto p2 = ImVec2(rectMax.x - radius, rectMax.y - radius); + auto* drawList = ImGui::GetWindowDrawList(); + + ImTextureID textureID = GetTextureID(shadowImage); + + if (drawTop) + drawList->AddImage(textureID, { p1.x - widthOffset, p1.y - radius }, { p2.x + widthOffset, p1.y }, ImVec2(0.0f, 1.0f), ImVec2(1.0f, 0.0f), ImColor(0.0f, 0.0f, 0.0f, alphaTop)); + if (drawBottom) + drawList->AddImage(textureID, { p1.x - widthOffset, p2.y }, { p2.x + widthOffset, p2.y + radius }, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), ImColor(0.0f, 0.0f, 0.0f, alphaBottom)); + if (drawLeft) + drawList->AddImageQuad(textureID, { p1.x - radius, p1.y - widthOffset }, { p1.x, p1.y - widthOffset }, { p1.x, p2.y + widthOffset }, { p1.x - radius, p2.y + widthOffset }, + { 0.0f, 1.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, ImColor(0.0f, 0.0f, 0.0f, alphaSides)); + if (drawRight) + drawList->AddImageQuad(textureID, { p2.x, p1.y - widthOffset }, { p2.x + radius, p1.y - widthOffset }, { p2.x + radius, p2.y + widthOffset }, { p2.x, p2.y + widthOffset }, + { 0.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, ImColor(0.0f, 0.0f, 0.0f, alphaSides)); + }; + + void DrawShadowInner(const Ref& shadowImage, int radius, ImRect rectangle, float alpha, float lengthStretch, + bool drawLeft, bool drawRight, bool drawTop, bool drawBottom) + { + DrawShadowInner(shadowImage, radius, rectangle.Min, rectangle.Max, alpha, lengthStretch, drawLeft, drawRight, drawTop, drawBottom); + }; + + + void DrawShadowInner(const Ref& shadowImage, int radius, float alpha, float lengthStretch, + bool drawLeft, bool drawRight, bool drawTop, bool drawBottom) + { + DrawShadowInner(shadowImage, radius, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), alpha, lengthStretch, drawLeft, drawRight, drawTop, drawBottom); + } + + + //========================================================================================= + bool FakeComboBoxButton(const char* label, const char* preview_value, ImGuiComboFlags flags) + { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = ImGui::GetCurrentWindow(); + + ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.WindowFlags; + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together + + const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : ImGui::GetFrameHeight(); + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ImGui::CalcItemWidth(); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + ImGui::ItemSize(total_bb, style.FramePadding.y); + if (!ImGui::ItemAdd(total_bb, id, &bb)) + return false; + + // Open on click + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held); + const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id); + bool popup_open = ImGui::IsPopupOpen(popup_id, ImGuiPopupFlags_None); + if (pressed && !popup_open) + { + //ImGui::OpenPopupEx(popup_id, ImGuiPopupFlags_None); + popup_open = true; + } + + // Render shape + const ImU32 frame_col = ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size); + ImGui::RenderNavHighlight(bb, id); + if (!(flags & ImGuiComboFlags_NoPreview)) + window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft); + if (!(flags & ImGuiComboFlags_NoArrowButton)) + { + ImU32 bg_col = ImGui::GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text); + window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight); + if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x) + ImGui::RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f); + } + ImGui::RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding); + + // Custom preview + if (flags & ImGuiComboFlags_CustomPreview) + { + g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y); + IM_ASSERT(preview_value == NULL || preview_value[0] == 0); + preview_value = NULL; + } + + // Render preview and label + if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview)) + { + if (g.LogEnabled) + ImGui::LogSetNextTextDecoration("{", "}"); + ImGui::RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL); + } + if (label_size.x > 0) + ImGui::RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label); + + if (!popup_open) + return false; + + g.NextWindowData.WindowFlags = backup_next_window_data_flags; + + return pressed; + } + + void DrawButtonImage(Ref imageNormal, Ref imageHovered, Ref imagePressed, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImVec2 rectMin, ImVec2 rectMax, ImVec2 uv0, ImVec2 uv1) + { + auto* drawList = ImGui::GetWindowDrawList(); + if (ImGui::IsItemActive()) + drawList->AddImage(GetTextureID(imagePressed), rectMin, rectMax, uv0, uv1, tintPressed); + else if (ImGui::IsItemHovered()) + drawList->AddImage(GetTextureID(imageHovered), rectMin, rectMax, uv0, uv1, tintHovered); + else + drawList->AddImage(GetTextureID(imageNormal), rectMin, rectMax, uv0, uv1, tintNormal); + }; + + void DrawButtonImage(Ref imageNormal, Ref imageHovered, Ref imagePressed, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImVec2 rectMin, ImVec2 rectMax, ImVec2 uv0, ImVec2 uv1) + { + auto* drawList = ImGui::GetWindowDrawList(); + if (ImGui::IsItemActive()) + drawList->AddImage(GetTextureID(imagePressed), rectMin, rectMax, uv0, uv1, tintPressed); + else if (ImGui::IsItemHovered()) + drawList->AddImage(GetTextureID(imageHovered), rectMin, rectMax, uv0, uv1, tintHovered); + else + drawList->AddImage(GetTextureID(imageNormal), rectMin, rectMax, uv0, uv1, tintNormal); + }; + +} diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiUtilities.h b/StarEngine/src/StarEngine/ImGui/ImGuiUtilities.h new file mode 100644 index 00000000..ab5a779d --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiUtilities.h @@ -0,0 +1,1044 @@ +#pragma once + +#include "StarEngine/ImGui/Colors.h" +#include "StarEngine/Renderer/Texture.h" + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include +#include + +#include +#include + +namespace StarEngine::UI { + //========================================================================================= + /// Utilities + ImTextureID GetTextureID(Ref texture); + + class ScopedStyle + { + public: + ScopedStyle(const ScopedStyle&) = delete; + ScopedStyle& operator=(const ScopedStyle&) = delete; + template + ScopedStyle(ImGuiStyleVar styleVar, T value) { ImGui::PushStyleVar(styleVar, value); } + ~ScopedStyle() { ImGui::PopStyleVar(); } + }; + + class ScopedColour + { + public: + ScopedColour(const ScopedColour&) = delete; + ScopedColour& operator=(const ScopedColour&) = delete; + template + ScopedColour(ImGuiCol colourId, T colour) { ImGui::PushStyleColor(colourId, ImColor(colour).Value); } + ~ScopedColour() { ImGui::PopStyleColor(); } + }; + + class ScopedFont + { + public: + ScopedFont(const ScopedFont&) = delete; + ScopedFont& operator=(const ScopedFont&) = delete; + ScopedFont(ImFont* font) { ImGui::PushFont(font); } + ~ScopedFont() { ImGui::PopFont(); } + }; + + class ScopedID + { + public: + ScopedID(const ScopedID&) = delete; + ScopedID& operator=(const ScopedID&) = delete; + template + ScopedID(T id) { ImGui::PushID(id); } + ~ScopedID() { ImGui::PopID(); } + }; + + template<> + inline ScopedID::ScopedID(UUID id) { ImGui::PushID(reinterpret_cast(static_cast(id))); } // because otherwise it will call PushID(int) which is not what we want + + class ScopedColourStack + { + public: + ScopedColourStack(const ScopedColourStack&) = delete; + ScopedColourStack& operator=(const ScopedColourStack&) = delete; + + template + ScopedColourStack(ImGuiCol firstColourID, ColourType firstColour, OtherColours&& ... otherColourPairs) + : m_Count((sizeof... (otherColourPairs) / 2) + 1) + { + static_assert ((sizeof... (otherColourPairs) & 1u) == 0, + "ScopedColourStack constructor expects a list of pairs of colour IDs and colours as its arguments"); + + PushColour(firstColourID, firstColour, std::forward(otherColourPairs)...); + } + + ~ScopedColourStack() { ImGui::PopStyleColor(m_Count); } + + private: + int m_Count; + + template + void PushColour(ImGuiCol colourID, ColourType colour, OtherColours&& ... otherColourPairs) + { + if constexpr (sizeof... (otherColourPairs) == 0) + { + ImGui::PushStyleColor(colourID, ImColor(colour).Value); + } + else + { + ImGui::PushStyleColor(colourID, ImColor(colour).Value); + PushColour(std::forward(otherColourPairs)...); + } + } + }; + + class ScopedStyleStack + { + public: + ScopedStyleStack(const ScopedStyleStack&) = delete; + ScopedStyleStack& operator=(const ScopedStyleStack&) = delete; + + template + ScopedStyleStack(ImGuiStyleVar firstStyleVar, ValueType firstValue, OtherStylePairs&& ... otherStylePairs) + : m_Count((sizeof... (otherStylePairs) / 2) + 1) + { + static_assert ((sizeof... (otherStylePairs) & 1u) == 0, + "ScopedStyleStack constructor expects a list of pairs of colour IDs and colours as its arguments"); + + PushStyle(firstStyleVar, firstValue, std::forward(otherStylePairs)...); + } + + ~ScopedStyleStack() { ImGui::PopStyleVar(m_Count); } + + private: + int m_Count; + + template + void PushStyle(ImGuiStyleVar styleVar, ValueType value, OtherStylePairs&& ... otherStylePairs) + { + if constexpr (sizeof... (otherStylePairs) == 0) + { + ImGui::PushStyleVar(styleVar, value); + } + else + { + ImGui::PushStyleVar(styleVar, value); + PushStyle(std::forward(otherStylePairs)...); + } + } + }; + + + class ScopedItemFlags + { + public: + ScopedItemFlags(const ScopedItemFlags&) = delete; + ScopedItemFlags& operator=(const ScopedItemFlags&) = delete; + ScopedItemFlags(const ImGuiItemFlags flags, const bool enable = true) + { + SE_CORE_VERIFY(!(flags & ImGuiItemFlags_Disabled), "We shouldn't use ImGuiItemFlags_Disabled! Use UI::BeginDisabled / UI::EndDisabled instead. It will handle visuals for you."); + ImGui::PushItemFlag(flags, enable); + } + ~ScopedItemFlags() { ImGui::PopItemFlag(); } + }; + + class ScopedDisable + { + public: + ScopedDisable(const ScopedDisable&) = delete; + ScopedDisable& operator=(const ScopedDisable&) = delete; + ScopedDisable(bool disabled = true); + ~ScopedDisable(); + }; + + // The delay won't work on texts, because the timer isn't tracked for them. + inline bool IsItemHovered(float delayInSeconds = 0.1f, ImGuiHoveredFlags flags = 0) + { + return ImGui::IsItemHovered() && GImGui->HoveredIdTimer > delayInSeconds; /*HoveredIdNotActiveTimer*/ + } + + inline void SetTooltip(std::string_view text, float delayInSeconds = 0.1f, bool allowWhenDisabled = true, ImVec2 padding = ImVec2(5, 5)) + { + if (IsItemHovered(delayInSeconds, allowWhenDisabled ? ImGuiHoveredFlags_AllowWhenDisabled : 0)) + { + UI::ScopedStyle tooltipPadding(ImGuiStyleVar_WindowPadding, padding); + UI::ScopedColour textCol(ImGuiCol_Text, Colors::Theme::textBrighter); + ImGui::SetTooltip(text.data()); + } + } + + // Check if navigated to current item, e.g. with arrow keys + inline bool NavigatedTo() + { + ImGuiContext& g = *GImGui; + return g.NavJustMovedToId == g.LastItemData.ID; + } + + + //========================================================================================= + /// Colours + + inline ImColor ColourWithValue(const ImColor& color, float value) + { + const ImVec4& colRaw = color.Value; + float hue, sat, val; + ImGui::ColorConvertRGBtoHSV(colRaw.x, colRaw.y, colRaw.z, hue, sat, val); + return ImColor::HSV(hue, sat, std::min(value, 1.0f)); + } + + inline ImColor ColourWithSaturation(const ImColor& color, float saturation) + { + const ImVec4& colRaw = color.Value; + float hue, sat, val; + ImGui::ColorConvertRGBtoHSV(colRaw.x, colRaw.y, colRaw.z, hue, sat, val); + return ImColor::HSV(hue, std::min(saturation, 1.0f), val); + } + + inline ImColor ColourWithHue(const ImColor& color, float hue) + { + const ImVec4& colRaw = color.Value; + float h, s, v; + ImGui::ColorConvertRGBtoHSV(colRaw.x, colRaw.y, colRaw.z, h, s, v); + return ImColor::HSV(std::min(hue, 1.0f), s, v); + } + + inline ImColor ColourWithAlpha(const ImColor& color, float multiplier) + { + ImVec4 colRaw = color.Value; + colRaw.w = multiplier; + return colRaw; + } + + inline ImColor ColourWithMultipliedValue(const ImColor& color, float multiplier) + { + const ImVec4& colRaw = color.Value; + float hue, sat, val; + ImGui::ColorConvertRGBtoHSV(colRaw.x, colRaw.y, colRaw.z, hue, sat, val); + return ImColor::HSV(hue, sat, std::min(val * multiplier, 1.0f)); + } + + inline ImColor ColourWithMultipliedSaturation(const ImColor& color, float multiplier) + { + const ImVec4& colRaw = color.Value; + float hue, sat, val; + ImGui::ColorConvertRGBtoHSV(colRaw.x, colRaw.y, colRaw.z, hue, sat, val); + return ImColor::HSV(hue, std::min(sat * multiplier, 1.0f), val); + } + + inline ImColor ColourWithMultipliedHue(const ImColor& color, float multiplier) + { + const ImVec4& colRaw = color.Value; + float hue, sat, val; + ImGui::ColorConvertRGBtoHSV(colRaw.x, colRaw.y, colRaw.z, hue, sat, val); + return ImColor::HSV(std::min(hue * multiplier, 1.0f), sat, val); + } + + inline ImColor ColourWithMultipliedAlpha(const ImColor& color, float multiplier) + { + ImVec4 colRaw = color.Value; + colRaw.w *= multiplier; + return colRaw; + } + + + // TODO: move most of the functions in this header into the Draw namespace + namespace Draw { + //========================================================================================= + /// Lines + inline void Underline(bool fullWidth = false, float offsetX = 0.0f, float offsetY = -1.0f) + { + if (fullWidth) + { + if (ImGui::GetCurrentWindow()->DC.CurrentColumns != nullptr) + ImGui::PushColumnsBackground(); + else if (ImGui::GetCurrentTable() != nullptr) + ImGui::TablePushBackgroundChannel(); + } + + const float width = fullWidth ? ImGui::GetWindowWidth() : ImGui::GetContentRegionAvail().x; + const ImVec2 cursor = ImGui::GetCursorScreenPos(); + ImGui::GetWindowDrawList()->AddLine(ImVec2(cursor.x + offsetX, cursor.y + offsetY), + ImVec2(cursor.x + width, cursor.y + offsetY), + Colors::Theme::backgroundDark, 1.0f); + + if (fullWidth) + { + if (ImGui::GetCurrentWindow()->DC.CurrentColumns != nullptr) + ImGui::PopColumnsBackground(); + else if (ImGui::GetCurrentTable() != nullptr) + ImGui::TablePopBackgroundChannel(); + } + } + } + + //========================================================================================= + /// Rectangle + + inline ImRect GetItemRect() + { + return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); + } + + inline ImRect RectExpanded(const ImRect& rect, float x, float y) + { + ImRect result = rect; + result.Min.x -= x; + result.Min.y -= y; + result.Max.x += x; + result.Max.y += y; + return result; + } + + inline ImRect RectOffset(const ImRect& rect, float x, float y) + { + ImRect result = rect; + result.Min.x += x; + result.Min.y += y; + result.Max.x += x; + result.Max.y += y; + return result; + } + + inline ImRect RectOffset(const ImRect& rect, ImVec2 xy) + { + return RectOffset(rect, xy.x, xy.y); + } + + //========================================================================================= + /// Window + + bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); + void EndPopup(); + + // MenuBar which allows you to specify its rectangle + bool BeginMenuBar(const ImRect& barRectangle); + void EndMenuBar(); + + // Exposed to be used for window with disabled decorations + // This border is going to be drawn even if window border size is set to 0.0f + void RenderWindowOuterBorders(ImGuiWindow* window); + + // Exposed resize behavior for native OS windows + bool UpdateWindowManualResize(ImGuiWindow* window, ImVec2& newSize, ImVec2& newPosition); + + // AKA ImGui::CollapsingHeader + bool ContextMenuHeader(const char* label, ImGuiTreeNodeFlags flags = 0); + + //========================================================================================= + /// Shadows + + void DrawShadow(const Ref& shadowImage, int radius, ImVec2 rectMin, ImVec2 rectMax, float alphMultiplier = 1.0f, float lengthStretch = 10.0f, + bool drawLeft = true, bool drawRight = true, bool drawTop = true, bool drawBottom = true); + + void DrawShadow(const Ref& shadowImage, int radius, ImRect rectangle, float alphMultiplier = 1.0f, float lengthStretch = 10.0f, + bool drawLeft = true, bool drawRight = true, bool drawTop = true, bool drawBottom = true); + + + void DrawShadow(const Ref& shadowImage, int radius, float alphMultiplier = 1.0f, float lengthStretch = 10.0f, + bool drawLeft = true, bool drawRight = true, bool drawTop = true, bool drawBottom = true); + + void DrawShadowInner(const Ref& shadowImage, int radius, ImVec2 rectMin, ImVec2 rectMax, float alpha = 1.0f, float lengthStretch = 10.0f, + bool drawLeft = true, bool drawRight = true, bool drawTop = true, bool drawBottom = true); + + void DrawShadowInner(const Ref& shadowImage, int radius, ImRect rectangle, float alpha = 1.0f, float lengthStretch = 10.0f, + bool drawLeft = true, bool drawRight = true, bool drawTop = true, bool drawBottom = true); + + + void DrawShadowInner(const Ref& shadowImage, int radius, float alpha = 1.0f, float lengthStretch = 10.0f, + bool drawLeft = true, bool drawRight = true, bool drawTop = true, bool drawBottom = true); + + + //========================================================================================= + // Outline + + inline void BeginDisabled(bool disabled = true) + { + if (disabled) + ImGui::BeginDisabled(true); + } + + inline bool IsItemDisabled() + { + return ImGui::GetItemFlags() & ImGuiItemFlags_Disabled; + } + + inline void EndDisabled() + { + // NOTE(Peter): Cheeky hack to prevent ImGui from asserting (required due to the nature of UI::BeginDisabled) + if (GImGui->DisabledStackSize > 0) + ImGui::EndDisabled(); + } + + typedef int OutlineFlags; + enum OutlineFlags_ + { + OutlineFlags_None = 0, // draw no activity outline + OutlineFlags_WhenHovered = 1 << 1, // draw an outline when item is hovered + OutlineFlags_WhenActive = 1 << 2, // draw an outline when item is active + OutlineFlags_WhenInactive = 1 << 3, // draw an outline when item is inactive + OutlineFlags_HighlightActive = 1 << 4, // when active, the outline is in highlight colour + OutlineFlags_NoHighlightActive = OutlineFlags_WhenHovered | OutlineFlags_WhenActive | OutlineFlags_WhenInactive, + OutlineFlags_NoOutlineInactive = OutlineFlags_WhenHovered | OutlineFlags_WhenActive | OutlineFlags_HighlightActive, + OutlineFlags_All = OutlineFlags_WhenHovered | OutlineFlags_WhenActive | OutlineFlags_WhenInactive | OutlineFlags_HighlightActive, + }; + + inline void DrawItemActivityOutline(OutlineFlags flags = OutlineFlags_All, ImColor colourHighlight = Colors::Theme::accent, float rounding = GImGui->Style.FrameRounding) + { + if (IsItemDisabled()) + return; + + auto* drawList = ImGui::GetWindowDrawList(); + const ImRect rect = RectExpanded(GetItemRect(), 1.0f, 1.0f); + if ((flags & OutlineFlags_WhenActive) && ImGui::IsItemActive()) + { + if (flags & OutlineFlags_HighlightActive) + { + drawList->AddRect(rect.Min, rect.Max, colourHighlight, rounding, 0, 1.5f); + } + else + { + drawList->AddRect(rect.Min, rect.Max, ImColor(60, 60, 60), rounding, 0, 1.5f); + } + } + else if ((flags & OutlineFlags_WhenHovered) && ImGui::IsItemHovered() && !ImGui::IsItemActive()) + { + drawList->AddRect(rect.Min, rect.Max, ImColor(60, 60, 60), rounding, 0, 1.5f); + } + else if ((flags & OutlineFlags_WhenInactive) && !ImGui::IsItemHovered() && !ImGui::IsItemActive()) + { + drawList->AddRect(rect.Min, rect.Max, ImColor(50, 50, 50), rounding, 0, 1.0f); + } + }; + + + //========================================================================================= + /// Property Fields + + // GetColFunction function takes 'int' index of the option to display and returns ImColor + template + bool PropertyDropdown(const char* label, const std::vector& options, int32_t optionCount, int32_t* selected, + GetColFunction getColourFunction) + { + const char* current = options[*selected].c_str(); + ImGui::Text(label); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + + bool changed = false; + + const std::string id = "##" + std::string(label); + + ImGui::PushStyleColor(ImGuiCol_Text, getColourFunction(*selected).Value); + + if (ImGui::BeginCombo(id.c_str(), current)) + { + for (int i = 0; i < optionCount; i++) + { + ImGui::PushStyleColor(ImGuiCol_Text, getColourFunction(i).Value); + + const bool is_selected = (current == options[i]); + if (ImGui::Selectable(options[i].c_str(), is_selected)) + { + current = options[i].c_str(); + *selected = i; + changed = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + + ImGui::PopStyleColor(); + } + ImGui::EndCombo(); + + } + ImGui::PopStyleColor(); + + UI::DrawItemActivityOutline(); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return changed; + }; + + inline void PopupMenuHeader(const std::string& text, bool indentAfter = true, bool unindendBefore = false) + { + if (unindendBefore) ImGui::Unindent(); + ImGui::TextColored(ImColor(170, 170, 170).Value, text.c_str()); + ImGui::Separator(); + if (indentAfter) ImGui::Indent(); + }; + + + //========================================================================================= + /// Button Image + + void DrawButtonImage(Ref imageNormal, Ref imageHovered, Ref imagePressed, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, ImVec2 rectMin, ImVec2 rectMax, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)); + + void DrawButtonImage(Ref imageNormal, Ref imageHovered, Ref imagePressed, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, ImVec2 rectMin, ImVec2 rectMax, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)); + + inline void DrawButtonImage(Ref imageNormal, Ref imageHovered, Ref imagePressed, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, ImRect rectangle, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(imageNormal, imageHovered, imagePressed, tintNormal, tintHovered, tintPressed, rectangle.Min, rectangle.Max, uv0, uv1); + }; + + inline void DrawButtonImage(Ref imageNormal, Ref imageHovered, Ref imagePressed, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImRect rectangle, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(imageNormal, imageHovered, imagePressed, tintNormal, tintHovered, tintPressed, rectangle.Min, rectangle.Max, uv0, uv1); + }; + + inline void DrawButtonImage(Ref image, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImVec2 rectMin, ImVec2 rectMax, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(image, image, image, tintNormal, tintHovered, tintPressed, rectMin, rectMax, uv0, uv1); + }; + + inline void DrawButtonImage(Ref image, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImRect rectangle, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(image, image, image, tintNormal, tintHovered, tintPressed, rectangle.Min, rectangle.Max, uv0, uv1); + }; + + inline void DrawButtonImage(Ref image, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImVec2 rectMin, ImVec2 rectMax, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(image, image, image, tintNormal, tintHovered, tintPressed, rectMin, rectMax, uv0, uv1); + }; + + inline void DrawButtonImage(Ref image, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImRect rectangle, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(image, image, image, tintNormal, tintHovered, tintPressed, rectangle.Min, rectangle.Max, uv0, uv1); + }; + + inline void DrawButtonImage(Ref imageNormal, Ref imageHovered, Ref imagePressed, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(imageNormal, imageHovered, imagePressed, tintNormal, tintHovered, tintPressed, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), uv0, uv1); + }; + + inline void DrawButtonImage(Ref image, + ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(image, image, image, tintNormal, tintHovered, tintPressed, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), uv0, uv1); + }; + + inline void DrawButtonImage(Ref image, ImU32 tintNormal, ImU32 tintHovered, ImU32 tintPressed, + ImVec2 uv0 = ImVec2(0.0f, 0.0f), ImVec2 uv1 = ImVec2(1.0f, 1.0f)) + { + DrawButtonImage(image, image, image, tintNormal, tintHovered, tintPressed, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), uv0, uv1); + }; + + + //========================================================================================= + /// Border + + inline void DrawBorder(ImVec2 rectMin, ImVec2 rectMax, const ImVec4& borderColour, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + auto min = rectMin; + min.x -= thickness; + min.y -= thickness; + min.x += offsetX; + min.y += offsetY; + auto max = rectMax; + max.x += thickness; + max.y += thickness; + max.x += offsetX; + max.y += offsetY; + + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRect(min, max, ImGui::ColorConvertFloat4ToU32(borderColour), 0.0f, 0, thickness); + }; + + inline void DrawBorder(const ImVec4& borderColour, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorder(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), borderColour, thickness, offsetX, offsetY); + }; + + inline void DrawBorder(float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorder(ImGui::GetStyleColorVec4(ImGuiCol_Border), thickness, offsetX, offsetY); + }; + + inline void DrawBorder(ImVec2 rectMin, ImVec2 rectMax, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorder(rectMin, rectMax, ImGui::GetStyleColorVec4(ImGuiCol_Border), thickness, offsetX, offsetY); + }; + + inline void DrawBorder(ImRect rect, float thickness = 1.0f, float rounding = 0.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + auto min = rect.Min; + min.x -= thickness; + min.y -= thickness; + min.x += offsetX; + min.y += offsetY; + auto max = rect.Max; + max.x += thickness; + max.y += thickness; + max.x += offsetX; + max.y += offsetY; + + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRect(min, max, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_Border)), rounding, 0, thickness); + }; + + inline void DrawBorderHorizontal(ImVec2 rectMin, ImVec2 rectMax, const ImVec4& borderColour, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + auto min = rectMin; + min.y -= thickness; + min.x += offsetX; + min.y += offsetY; + auto max = rectMax; + max.y += thickness; + max.x += offsetX; + max.y += offsetY; + + auto* drawList = ImGui::GetWindowDrawList(); + const auto colour = ImGui::ColorConvertFloat4ToU32(borderColour); + drawList->AddLine(min, ImVec2(max.x, min.y), colour, thickness); + drawList->AddLine(ImVec2(min.x, max.y), max, colour, thickness); + }; + + inline void DrawBorderHorizontal(ImVec2 rectMin, ImVec2 rectMax, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorderHorizontal(rectMin, rectMax, ImGui::GetStyleColorVec4(ImGuiCol_Border), thickness, offsetX, offsetY); + }; + + inline void DrawBorderHorizontal(const ImVec4& borderColour, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorderHorizontal(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), borderColour, thickness, offsetX, offsetY); + }; + + inline void DrawBorderHorizontal(float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorderHorizontal(ImGui::GetStyleColorVec4(ImGuiCol_Border), thickness, offsetX, offsetY); + }; + + inline void DrawBorderVertical(ImVec2 rectMin, ImVec2 rectMax, const ImVec4& borderColour, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + auto min = rectMin; + min.x -= thickness; + min.x += offsetX; + min.y += offsetY; + auto max = rectMax; + max.x += thickness; + max.x += offsetX; + max.y += offsetY; + + auto* drawList = ImGui::GetWindowDrawList(); + const auto colour = ImGui::ColorConvertFloat4ToU32(borderColour); + drawList->AddLine(min, ImVec2(min.x, max.y), colour, thickness); + drawList->AddLine(ImVec2(max.x, min.y), max, colour, thickness); + }; + + inline void DrawBorderVertical(const ImVec4& borderColour, float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorderVertical(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), borderColour, thickness, offsetX, offsetY); + }; + + inline void DrawBorderVertical(float thickness = 1.0f, float offsetX = 0.0f, float offsetY = 0.0f) + { + DrawBorderVertical(ImGui::GetStyleColorVec4(ImGuiCol_Border), thickness, offsetX, offsetY); + }; + + + //========================================================================================= + /// Custom IMGui controls + + inline const char* PatchFormatStringFloatToInt(const char* fmt) + { + if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '0' && fmt[3] == 'f' && fmt[4] == 0) // Fast legacy path for "%.0f" which is expected to be the most common case. + return "%d"; + const char* fmt_start = ImParseFormatFindStart(fmt); // Find % (if any, and ignore %%) + const char* fmt_end = ImParseFormatFindEnd(fmt_start); // Find end of format specifier, which itself is an exercise of confidence/recklessness (because snprintf is dependent on libc or user). + if (fmt_end > fmt_start && fmt_end[-1] == 'f') + { +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // TODO(Yan): what is this + if (fmt_start == fmt && fmt_end[0] == 0) + return "%d"; + ImGuiContext& g = *GImGui; + //ImFormatString(g.TempBuffer, g.TempBuffer.Size, "%.*s%%d%s", (int)(fmt_start - fmt), fmt, fmt_end); // Honor leading and trailing decorations, but lose alignment/precision. + //return g.TempBuffer; + return nullptr; +#else + IM_ASSERT(0 && "DragInt(): Invalid format string!"); // Old versions used a default parameter of "%.0f", please replace with e.g. "%d" +#endif + } + return fmt; + } + + inline int FormatString(char* buf, size_t buf_size, const char* fmt, ...) + { + va_list args; + va_start(args, fmt); +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + va_end(args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; + } + + inline bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = ImGui::CalcItemWidth(); + + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, ImVec2(window->DC.CursorPos.x + w, window->DC.CursorPos.y + (label_size.y + style.FramePadding.y * 2.0f))); + const ImRect total_bb(frame_bb.Min, ImVec2(frame_bb.Max.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_bb.Max.y)); + + const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; + ImGui::ItemSize(total_bb, style.FramePadding.y); + if (!ImGui::ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) + return false; + + // Default format string when passing NULL + if (format == NULL) + format = ImGui::DataTypeGetInfo(data_type)->PrintFmt; + else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.) + format = PatchFormatStringFloatToInt(format); + + // Tabbing or CTRL-clicking on Drag turns it into an InputText + const bool hovered = ImGui::ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags); + bool temp_input_is_active = temp_input_allowed && ImGui::TempInputIsActive(id); + if (!temp_input_is_active) + { + const bool clicked = (hovered && g.IO.MouseClicked[0]); + const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2); + if (clicked || double_clicked || g.NavActivateId == id) + { + ImGui::SetActiveID(id, window); + ImGui::SetFocusID(id, window); + ImGui::FocusWindow(window); + g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + if (temp_input_allowed) + if ((clicked && g.IO.KeyCtrl) || double_clicked) + temp_input_is_active = true; + } + + // Experimental: simple click (without moving) turns Drag into an InputText + if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active) + if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !ImGui::IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * 0.5f)) + { + g.NavActivateId = id; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + temp_input_is_active = true; + } + } + + if (temp_input_is_active) + { + // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set + const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0 && (p_min == NULL || p_max == NULL || ImGui::DataTypeCompare(data_type, p_min, p_max) < 0); + return ImGui::TempInputScalar(frame_bb, id, label, data_type, p_data, format, is_clamp_input ? p_min : NULL, is_clamp_input ? p_max : NULL); + } + + // Draw frame + const ImU32 frame_col = ImGui::GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + ImGui::RenderNavHighlight(frame_bb, id); + ImGui::RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); + + // Drag behavior + const bool value_changed = ImGui::DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags); + if (value_changed) + ImGui::MarkItemEdited(id); + + const bool mixed_value = (g.CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0; + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + (mixed_value ? FormatString(value_buf, IM_ARRAYSIZE(value_buf), "---") : ImGui::DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format)); + if (g.LogEnabled) + ImGui::LogSetNextTextDecoration("{", "}"); + ImGui::RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); + + if (label_size.x > 0.0f) + ImGui::RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + DrawItemActivityOutline(); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return value_changed; + } + + inline bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0) + { + bool changed = ImGui::DragScalarN(label, data_type, p_data, components, v_speed, p_min, p_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + bool changed = ImGui::SliderFloat(label, v, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + bool changed = ImGui::DragFloat2(label, v, v_speed, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + bool changed = ImGui::SliderFloat2(label, v, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + bool changed = ImGui::DragFloat3(label, v, v_speed, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0) { + bool changed = ImGui::SliderFloat3(label, v, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + bool changed = ImGui::DragFloat4(label, v, v_speed, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0) { + bool changed = ImGui::SliderFloat4(label, v, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool DragDouble(const char* label, double* v, float v_speed = 1.0f, double v_min = 0.0, double v_max = 0.0, const char* format = "%.3f", ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_Double, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool DragInt8(const char* label, int8_t* v, float v_speed = 1.0f, int8_t v_min = 0, int8_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_S8, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool DragInt16(const char* label, int16_t* v, float v_speed = 1.0f, int16_t v_min = 0, int16_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_S16, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool DragInt32(const char* label, int32_t* v, float v_speed = 1.0f, int32_t v_min = 0, int32_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool SliderInt32(const char* label, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0) + { + bool changed = ImGui::SliderInt(label, v, v_min, v_max, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool DragInt64(const char* label, int64_t* v, float v_speed = 1.0f, int64_t v_min = 0, int64_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_S64, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool DragUInt8(const char* label, uint8_t* v, float v_speed = 1.0f, uint8_t v_min = 0, uint8_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_U8, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool DragUInt16(const char* label, uint16_t* v, float v_speed = 1.0f, uint16_t v_min = 0, uint16_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_U16, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool DragUInt32(const char* label, uint32_t* v, float v_speed = 1.0f, uint32_t v_min = 0, uint32_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_U32, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool DragUInt64(const char* label, uint64_t* v, float v_speed = 1.0f, uint64_t v_min = 0, uint64_t v_max = 0, const char* format = nullptr, ImGuiSliderFlags flags = 0) + { + return DragScalar(label, ImGuiDataType_U64, v, v_speed, &v_min, &v_max, format, flags); + } + + inline bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0) + { + bool changed = ImGui::InputScalar(label, data_type, p_data, p_step, p_step_fast, format, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, const char* format = "%.3f", ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_Float, v, &step, &step_fast, format, flags); + } + + inline bool InputDouble(const char* label, double* v, double step = 0.0, double step_fast = 0.0, const char* format = "%.6f", ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_Double, v, &step, &step_fast, format, flags); + } + + inline bool InputInt8(const char* label, int8_t* v, int8_t step = 1, int8_t step_fast = 1, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_S8, v, &step, &step_fast, nullptr, flags); + } + + inline bool InputInt16(const char* label, int16_t* v, int16_t step = 1, int16_t step_fast = 10, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_S16, v, &step, &step_fast, nullptr, flags); + } + + inline bool InputInt32(const char* label, int32_t* v, int32_t step = 1, int32_t step_fast = 100, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_S32, v, &step, &step_fast, nullptr, flags); + } + + inline bool InputInt64(const char* label, int64_t* v, int64_t step = 1, int64_t step_fast = 1000, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_S64, v, &step, &step_fast, nullptr, flags); + } + + inline bool InputUInt8(const char* label, uint8_t* v, uint8_t step = 1, uint8_t step_fast = 1, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_U8, v, &step, &step_fast, nullptr, flags); + } + + inline bool InputUInt16(const char* label, uint16_t* v, uint16_t step = 1, uint16_t step_fast = 10, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_U16, v, &step, &step_fast, nullptr, flags); + } + + inline bool InputUInt32(const char* label, uint32_t* v, uint32_t step = 1, uint32_t step_fast = 100, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_U32, v, &step, &step_fast, nullptr, flags); + } + + inline bool InputUInt64(const char* label, uint64_t* value, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_U64, value, nullptr, nullptr, nullptr, flags); + } + + inline bool InputUInt64(const char* label, uint64_t* v, uint64_t step = 0, uint64_t step_fast = 0, ImGuiInputTextFlags flags = 0) + { + return InputScalar(label, ImGuiDataType_U64, v, step ? &step : nullptr, step_fast ? &step_fast : nullptr, nullptr, flags); + } + + inline bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL) + { + bool changed = ImGui::InputText(label, buf, buf_size, flags, callback, user_data); + DrawItemActivityOutline(); + return changed; + } + + inline bool InputText(const char* label, std::string* value, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL) + { + bool changed = ImGui::InputText(label, value, flags, callback, user_data); + DrawItemActivityOutline(); + return changed; + } + + inline bool InputTextMultiline(const char* label, std::string* value, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL) + { + bool changed = ImGui::InputTextMultiline(label, value, size, flags, callback, user_data); + DrawItemActivityOutline(); + return changed; + } + + inline bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0) + { + bool changed = ImGui::ColorEdit3(label, col, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0) + { + bool changed = ImGui::ColorEdit4(label, col, flags); + DrawItemActivityOutline(); + return changed; + } + + inline bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0) + { + bool opened = ImGui::BeginCombo(label, preview_value, flags); + DrawItemActivityOutline(); + return opened; + } + + inline void EndCombo() + { + ImGui::EndCombo(); + } + + bool FakeComboBoxButton(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); + + + inline bool Checkbox(const char* label, bool* b) + { + bool changed = ImGui::Checkbox(label, b); + UI::DrawItemActivityOutline(); + return changed; + } + + + inline bool Hyperlink(const char* label, ImU32 lineColor = Colors::Theme::accent, float lineThickness = GImGui->Style.FrameBorderSize) + { + ImGui::Text(label); + const ImRect rect = RectExpanded(GetItemRect(), lineThickness, lineThickness); + if (ImGui::IsItemHovered()) + { + ImGui::GetWindowDrawList()->AddLine({ rect.Min.x, rect.Max.y }, rect.Max, lineColor, lineThickness); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + return ImGui::IsMouseReleased(ImGuiMouseButton_Left); + } + return false; + } + +} // namespace StarEngine::UI diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiWidgets.cpp b/StarEngine/src/StarEngine/ImGui/ImGuiWidgets.cpp new file mode 100644 index 00000000..d7ed1b5b --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiWidgets.cpp @@ -0,0 +1 @@ +#include "sepch.h" diff --git a/StarEngine/src/StarEngine/ImGui/ImGuiWidgets.h b/StarEngine/src/StarEngine/ImGui/ImGuiWidgets.h new file mode 100644 index 00000000..9f20c353 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuiWidgets.h @@ -0,0 +1,309 @@ +#pragma once + +#include "UICore.h" +#include "ImGuiUtilities.h" +#include "StarEngine/Core/Input.h" +#include "StarEngine/Scripting/ScriptEngine.h" +#include "StarEngine/Editor/EditorResources.h" +#include "StarEngine/Scene/Scene.h" +#include "StarEngine/Utilities/StringUtils.h" + +#include "choc/text/choc_StringUtilities.h" + +#include +using namespace magic_enum::bitwise_operators; + +namespace StarEngine::UI +{ + enum class VectorAxis + { + None = 0, + X = BIT(0), + Y = BIT(1), + Z = BIT(2), + W = BIT(3) + }; + + static bool IsMatchingSearch(const std::string& item, std::string_view searchQuery, bool caseSensitive = false, bool stripWhiteSpaces = false, bool stripUnderscores = false) + { + if (searchQuery.empty()) + return true; + + if (item.empty()) + return false; + + std::string itemSanitized = stripUnderscores ? choc::text::replace(item, "_", " ") : item; + + if (stripWhiteSpaces) + itemSanitized = choc::text::replace(itemSanitized, " ", ""); + + std::string searchString = stripWhiteSpaces ? choc::text::replace(searchQuery, " ", "") : std::string(searchQuery); + + if (!caseSensitive) + { + itemSanitized = Utils::String::ToLower(itemSanitized); + searchString = Utils::String::ToLower(searchString); + } + + bool result = false; + if (choc::text::contains(searchString, " ")) + { + std::vector searchTerms = choc::text::splitAtWhitespace(searchString); + for (const auto& searchTerm : searchTerms) + { + if (!searchTerm.empty() && choc::text::contains(itemSanitized, searchTerm)) + result = true; + else + { + result = false; + break; + } + } + } + else + { + result = choc::text::contains(itemSanitized, searchString); + } + + return result; + } + + class Widgets + { + public: + template + static bool SearchWidget(StringType& searchString, const char* hint = "Search...", bool* grabFocus = nullptr) + { + PushID(); + + ShiftCursorY(1.0f); + + const bool layoutSuspended = [] + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->DC.CurrentLayout) + { + ImGui::SuspendLayout(); + return true; + } + return false; + }(); + + bool modified = false; + bool searching = false; + + const float areaPosX = ImGui::GetCursorPosX(); + const float framePaddingY = ImGui::GetStyle().FramePadding.y; + + UI::ScopedStyle rounding(ImGuiStyleVar_FrameRounding, 3.0f); + UI::ScopedStyle padding(ImGuiStyleVar_FramePadding, ImVec2(28.0f, framePaddingY)); + + if constexpr (std::is_same::value) + { + char searchBuffer[BuffSize + 1]{}; + strncpy(searchBuffer, searchString.c_str(), BuffSize); + if (ImGui::InputText(GenerateID(), searchBuffer, BuffSize)) + { + searchString = searchBuffer; + modified = true; + } + else if (ImGui::IsItemDeactivatedAfterEdit()) + { + searchString = searchBuffer; + modified = true; + } + + searching = searchBuffer[0] != 0; + } + else + { + static_assert(std::is_same::value, + "searchString paramenter must be std::string& or char*"); + + if (ImGui::InputText(GenerateID(), searchString, BuffSize)) + { + modified = true; + } + else if (ImGui::IsItemDeactivatedAfterEdit()) + { + modified = true; + } + + searching = searchString[0] != 0; + } + + if (grabFocus && *grabFocus) + { + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) + && !ImGui::IsAnyItemActive() + && !ImGui::IsMouseClicked(0)) + { + ImGui::SetKeyboardFocusHere(-1); + } + + if (ImGui::IsItemFocused()) + *grabFocus = false; + } + + UI::DrawItemActivityOutline(); + ImGui::SetItemAllowOverlap(); + + ImGui::SameLine(areaPosX + 5.0f); + + if (layoutSuspended) + ImGui::ResumeLayout(); + + ImGui::BeginHorizontal(GenerateID(), ImGui::GetItemRectSize()); + const ImVec2 iconSize(ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight()); + + // Search icon + { + const float iconYOffset = framePaddingY - 3.0f; + UI::ShiftCursorY(iconYOffset); + UI::Image(EditorResources::SearchIcon, iconSize, ImVec2(0, 0), ImVec2(1, 1), ImVec4(1.0f, 1.0f, 1.0f, 0.2f)); + UI::ShiftCursorY(-iconYOffset); + + // Hint + if (!searching) + { + UI::ShiftCursorY(-framePaddingY + 1.0f); + UI::ScopedColour text(ImGuiCol_Text, Colors::Theme::textDarker); + UI::ScopedStyle padding(ImGuiStyleVar_FramePadding, ImVec2(0.0f, framePaddingY)); + ImGui::TextUnformatted(hint); + UI::ShiftCursorY(-1.0f); + } + } + + ImGui::Spring(); + + // Clear icon + if (searching) + { + const float spacingX = 4.0f; + const float lineHeight = ImGui::GetItemRectSize().y - framePaddingY / 2.0f; + + if (ImGui::InvisibleButton(GenerateID(), ImVec2{ lineHeight, lineHeight })) + { + if constexpr (std::is_same::value) + searchString.clear(); + else + memset(searchString, 0, BuffSize); + + modified = true; + } + + if (ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax())) + ImGui::SetMouseCursor(ImGuiMouseCursor_Arrow); + + UI::DrawButtonImage(EditorResources::ClearIcon, IM_COL32(160, 160, 160, 200), + IM_COL32(170, 170, 170, 255), + IM_COL32(160, 160, 160, 150), + UI::RectExpanded(UI::GetItemRect(), -2.0f, -2.0f)); + + ImGui::Spring(-1.0f, spacingX * 2.0f); + } + + ImGui::EndHorizontal(); + UI::ShiftCursorY(-1.0f); + UI::PopID(); + return modified; + } + + static bool AssetSearchPopup(const char* ID, AssetHandle& selected, bool* cleared = nullptr, const char* hint = "Search Assets", ImVec2 size = ImVec2{ 250.0f, 350.0f }, std::initializer_list assetTypes = {}); + static bool AssetSearchPopup(const char* ID, AssetType assetType, AssetHandle& selected, bool* cleared = nullptr, const char* hint = "Search Assets", ImVec2 size = ImVec2{ 250.0f, 350.0f }); + static bool EntitySearchPopup(const char* ID, Ref scene, UUID& selected, bool* cleared = nullptr, const char* hint = "Search Entities", ImVec2 size = ImVec2{ 250.0f, 350.0f }); + static bool ScriptSearchPopup(const char* ID, const ScriptEngine& scriptEngine, UUID& selected, bool* cleared = nullptr, const char* hint = "Search Scripts", ImVec2 size = ImVec2{ 250.0f, 350.0f }); + + static bool OptionsButton() + { + const bool clicked = ImGui::InvisibleButton("##options", ImVec2{ ImGui::GetFrameHeight(), ImGui::GetFrameHeight() }); + + const float spaceAvail = std::min(ImGui::GetItemRectSize().x, ImGui::GetItemRectSize().y); + const float desiredIconSize = 15.0f; + const float padding = std::max((spaceAvail - desiredIconSize) / 2.0f, 0.0f); + + constexpr auto buttonColour = Colors::Theme::text; + const uint8_t value = uint8_t(ImColor(buttonColour).Value.x * 255); + UI::DrawButtonImage(EditorResources::GearIcon, IM_COL32(value, value, value, 200), + IM_COL32(value, value, value, 255), + IM_COL32(value, value, value, 150), + UI::RectExpanded(UI::GetItemRect(), -padding, -padding)); + return clicked; + } + + // See also ImGuiUtilities.h UI::DragVec3() which uses Dear ImGui's internal DragVec3 + // By contrast EditVec3() custom draws the X,Y, and Z components with red, green and blue label buttons. + static bool EditVec3(std::string_view label, ImVec2 size, float resetValue, bool& manuallyEdited, glm::vec3& value, VectorAxis renderMultiSelectAxes = VectorAxis::None, float speed = 1.0f, glm::vec3 v_min = glm::zero(), glm::vec3 v_max = glm::zero(), const char* format = "%.2f", ImGuiSliderFlags flags = 0) + { + ImGui::BeginVertical((std::string(label) + "fr").data()); + bool changed = false; + { + const float spacingX = 8.0f; + UI::ScopedStyle itemSpacing(ImGuiStyleVar_ItemSpacing, ImVec2{ spacingX, 0.0f }); + UI::ScopedStyle padding(ImGuiStyleVar_WindowPadding, ImVec2{ 0.0f, 2.0f }); + const float framePadding = 2.0f; + const float outlineSpacing = 1.0f; + const float lineHeight = GImGui->Font->FontSize + framePadding * 2.0f; + const ImVec2 buttonSize = { lineHeight + 2.0f, lineHeight }; + const float inputItemWidth = size.x / 3.0f - buttonSize.x; + + UI::ShiftCursorY(framePadding); + + const ImGuiIO& io = ImGui::GetIO(); + auto boldFont = io.Fonts->Fonts[0]; + + auto drawControl = [&](const std::string& label, float& value, const ImVec4& colourN, const ImVec4& colourH, const ImVec4& colourP, bool renderMultiSelect, float speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) + { + { + UI::ScopedStyle buttonFrame(ImGuiStyleVar_FramePadding, ImVec2(framePadding, 0.0f)); + UI::ScopedStyle buttonRounding(ImGuiStyleVar_FrameRounding, 1.0f); + UI::ScopedColourStack buttonColours(ImGuiCol_Button, colourN, ImGuiCol_ButtonHovered, colourH, ImGuiCol_ButtonActive, colourP); + + UI::ScopedFont buttonFont(boldFont); + + //ImGui::AlignTextToFramePadding(); + UI::ShiftCursorY(framePadding / 2.0f); + if (ImGui::Button(label.c_str(), buttonSize)) + { + value = resetValue; + changed = true; + } + } + + ImGui::SameLine(0.0f, outlineSpacing); + ImGui::SetNextItemWidth(inputItemWidth); + UI::ShiftCursorY(-framePadding / 2.0f); + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, renderMultiSelect); + bool wasTempInputActive = ImGui::TempInputIsActive(ImGui::GetID(("##" + label).c_str())); + changed |= UI::DragFloat(("##" + label).c_str(), &value, speed, v_min, v_max, format, flags); + + // NOTE(Peter): Ugly hack to make tabbing behave the same as Enter (e.g marking it as manually modified) + if (changed && Input::IsKeyDown(KeyCode::Tab)) + manuallyEdited = true; + + if (ImGui::TempInputIsActive(ImGui::GetID(("##" + label).c_str()))) + changed = false; + + ImGui::PopItemFlag(); + + if (wasTempInputActive) + manuallyEdited |= ImGui::IsItemDeactivatedAfterEdit(); + }; + + drawControl("X", value.x, ImVec4{ 0.8f, 0.1f, 0.15f, 1.0f }, ImVec4{ 0.9f, 0.2f, 0.2f, 1.0f }, ImVec4{ 0.8f, 0.1f, 0.15f, 1.0f }, (renderMultiSelectAxes & VectorAxis::X) == VectorAxis::X, speed, v_min.x, v_max.x, format, flags); + + ImGui::SameLine(0.0f, outlineSpacing); + drawControl("Y", value.y, ImVec4{ 0.2f, 0.7f, 0.2f, 1.0f }, ImVec4{ 0.3f, 0.8f, 0.3f, 1.0f }, ImVec4{ 0.2f, 0.7f, 0.2f, 1.0f }, (renderMultiSelectAxes & VectorAxis::Y) == VectorAxis::Y, speed, v_min.y, v_max.y, format, flags); + + ImGui::SameLine(0.0f, outlineSpacing); + drawControl("Z", value.z, ImVec4{ 0.1f, 0.25f, 0.8f, 1.0f }, ImVec4{ 0.2f, 0.35f, 0.9f, 1.0f }, ImVec4{ 0.1f, 0.25f, 0.8f, 1.0f }, (renderMultiSelectAxes & VectorAxis::Z) == VectorAxis::Z, speed, v_min.z, v_max.z, format, flags); + + // ImGui::EndChild(); + ImGui::EndVertical(); + } + return changed || manuallyEdited; + } + + }; // Widgets + +} // namespace StarEngine::UI diff --git a/StarEngine/src/StarEngine/ImGui/ImGuizmo.cpp b/StarEngine/src/StarEngine/ImGui/ImGuizmo.cpp new file mode 100644 index 00000000..d578594f --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuizmo.cpp @@ -0,0 +1,2750 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v 1.83 +// +// The MIT License(MIT) +// +// Copyright(c) 2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#include "sepch.h" +#include "ImGuizmo.h" + +#include "StarEngine/Core/Input.h" +#include "StarEngine/Core/KeyCodes.h" + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include + +#if defined(_MSC_VER) || defined(__MINGW32__) +#include +#endif +#if !defined(_MSC_VER) && !defined(__MINGW64_VERSION_MAJOR) +#define _malloca(x) alloca(x) +#define _freea(x) +#endif + +// includes patches for multiview from +// https://github.com/CedricGuillemet/ImGuizmo/issues/15 + +namespace ImGuizmo +{ + static const float ZPI = 3.14159265358979323846f; + static const float RAD2DEG = (180.f / ZPI); + static const float DEG2RAD = (ZPI / 180.f); + const float screenRotateSize = 0.06f; + + static OPERATION operator&(OPERATION lhs, OPERATION rhs) + { + return static_cast(static_cast(lhs) & static_cast(rhs)); + } + + static bool operator!=(OPERATION lhs, int rhs) + { + return static_cast(lhs) != rhs; + } + + static bool operator==(OPERATION lhs, int rhs) + { + return static_cast(lhs) == rhs; + } + + static bool Intersects(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) != 0; + } + + // True if lhs contains rhs + static bool Contains(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) == rhs; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // utility and math + + void FPU_MatrixF_x_MatrixF(const float* a, const float* b, float* r) + { + r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; + r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; + r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; + r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; + + r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; + r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; + r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; + r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; + + r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; + r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; + r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; + r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; + + r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; + r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; + r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; + r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; + } + + void Frustum(float left, float right, float bottom, float top, float znear, float zfar, float* m16) + { + float temp, temp2, temp3, temp4; + temp = 2.0f * znear; + temp2 = right - left; + temp3 = top - bottom; + temp4 = zfar - znear; + m16[0] = temp / temp2; + m16[1] = 0.0; + m16[2] = 0.0; + m16[3] = 0.0; + m16[4] = 0.0; + m16[5] = temp / temp3; + m16[6] = 0.0; + m16[7] = 0.0; + m16[8] = (right + left) / temp2; + m16[9] = (top + bottom) / temp3; + m16[10] = (-zfar - znear) / temp4; + m16[11] = -1.0f; + m16[12] = 0.0; + m16[13] = 0.0; + m16[14] = (-temp * zfar) / temp4; + m16[15] = 0.0; + } + + void Perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar, float* m16) + { + float ymax, xmax; + ymax = znear * tanf(fovyInDegrees * DEG2RAD); + xmax = ymax * aspectRatio; + Frustum(-xmax, xmax, -ymax, ymax, znear, zfar, m16); + } + + void Cross(const float* a, const float* b, float* r) + { + r[0] = a[1] * b[2] - a[2] * b[1]; + r[1] = a[2] * b[0] - a[0] * b[2]; + r[2] = a[0] * b[1] - a[1] * b[0]; + } + + float Dot(const float* a, const float* b) + { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + + void Normalize(const float* a, float* r) + { + float il = 1.f / (sqrtf(Dot(a, a)) + FLT_EPSILON); + r[0] = a[0] * il; + r[1] = a[1] * il; + r[2] = a[2] * il; + } + + void LookAt(const float* eye, const float* at, const float* up, float* m16) + { + float X[3], Y[3], Z[3], tmp[3]; + + tmp[0] = eye[0] - at[0]; + tmp[1] = eye[1] - at[1]; + tmp[2] = eye[2] - at[2]; + Normalize(tmp, Z); + Normalize(up, Y); + Cross(Y, Z, tmp); + Normalize(tmp, X); + Cross(Z, X, tmp); + Normalize(tmp, Y); + + m16[0] = X[0]; + m16[1] = Y[0]; + m16[2] = Z[0]; + m16[3] = 0.0f; + m16[4] = X[1]; + m16[5] = Y[1]; + m16[6] = Z[1]; + m16[7] = 0.0f; + m16[8] = X[2]; + m16[9] = Y[2]; + m16[10] = Z[2]; + m16[11] = 0.0f; + m16[12] = -Dot(X, eye); + m16[13] = -Dot(Y, eye); + m16[14] = -Dot(Z, eye); + m16[15] = 1.0f; + } + + template T Clamp(T x, T y, T z) { return ((x < y) ? y : ((x > z) ? z : x)); } + template T max(T x, T y) { return (x > y) ? x : y; } + template T min(T x, T y) { return (x < y) ? x : y; } + template bool IsWithin(T x, T y, T z) { return (x >= y) && (x <= z); } + + struct matrix_t; + struct vec_t + { + public: + float x, y, z, w; + + void Lerp(const vec_t& v, float t) + { + x += (v.x - x) * t; + y += (v.y - y) * t; + z += (v.z - z) * t; + w += (v.w - w) * t; + } + + void Set(float v) { x = y = z = w = v; } + void Set(float _x, float _y, float _z = 0.f, float _w = 0.f) { x = _x; y = _y; z = _z; w = _w; } + + vec_t& operator -= (const vec_t& v) { x -= v.x; y -= v.y; z -= v.z; w -= v.w; return *this; } + vec_t& operator += (const vec_t& v) { x += v.x; y += v.y; z += v.z; w += v.w; return *this; } + vec_t& operator *= (const vec_t& v) { x *= v.x; y *= v.y; z *= v.z; w *= v.w; return *this; } + vec_t& operator *= (float v) { x *= v; y *= v; z *= v; w *= v; return *this; } + + vec_t operator * (float f) const; + vec_t operator - () const; + vec_t operator - (const vec_t& v) const; + vec_t operator + (const vec_t& v) const; + vec_t operator * (const vec_t& v) const; + + const vec_t& operator + () const { return (*this); } + float Length() const { return sqrtf(x * x + y * y + z * z); }; + float LengthSq() const { return (x * x + y * y + z * z); }; + vec_t Normalize() { (*this) *= (1.f / Length()); return (*this); } + vec_t Normalize(const vec_t& v) { this->Set(v.x, v.y, v.z, v.w); this->Normalize(); return (*this); } + vec_t Abs() const; + + void Cross(const vec_t& v) + { + vec_t res; + res.x = y * v.z - z * v.y; + res.y = z * v.x - x * v.z; + res.z = x * v.y - y * v.x; + + x = res.x; + y = res.y; + z = res.z; + w = 0.f; + } + + void Cross(const vec_t& v1, const vec_t& v2) + { + x = v1.y * v2.z - v1.z * v2.y; + y = v1.z * v2.x - v1.x * v2.z; + z = v1.x * v2.y - v1.y * v2.x; + w = 0.f; + } + + float Dot(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w); + } + + float Dot3(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z); + } + + void Transform(const matrix_t& matrix); + void Transform(const vec_t& s, const matrix_t& matrix); + + void TransformVector(const matrix_t& matrix); + void TransformPoint(const matrix_t& matrix); + void TransformVector(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformVector(matrix); } + void TransformPoint(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformPoint(matrix); } + + float& operator [] (size_t index) { return ((float*)&x)[index]; } + const float& operator [] (size_t index) const { return ((float*)&x)[index]; } + //bool operator!=(const vec_t& other) const { return memcmp(this, &other, sizeof(vec_t)); } + bool operator!=(const vec_t& other) const + { + // allow some tolerance. exact equality is too strict and results in stupidly small "deltas" counting as a manipulation + return (fabs(other.x - x) > 0.000001f) || (fabs(other.y - y) > 0.000001f) || (fabs(other.z - z) > 0.000001f) || (fabs(other.w - w) > 0.000001f); + } + }; + + vec_t makeVect(float _x, float _y, float _z = 0.f, float _w = 0.f) { vec_t res; res.x = _x; res.y = _y; res.z = _z; res.w = _w; return res; } + vec_t makeVect(ImVec2 v) { vec_t res; res.x = v.x; res.y = v.y; res.z = 0.f; res.w = 0.f; return res; } + vec_t vec_t::operator * (float f) const { return makeVect(x * f, y * f, z * f, w * f); } + vec_t vec_t::operator - () const { return makeVect(-x, -y, -z, -w); } + vec_t vec_t::operator - (const vec_t& v) const { return makeVect(x - v.x, y - v.y, z - v.z, w - v.w); } + vec_t vec_t::operator + (const vec_t& v) const { return makeVect(x + v.x, y + v.y, z + v.z, w + v.w); } + vec_t vec_t::operator * (const vec_t& v) const { return makeVect(x * v.x, y * v.y, z * v.z, w * v.w); } + vec_t vec_t::Abs() const { return makeVect(fabsf(x), fabsf(y), fabsf(z)); } + + vec_t Normalized(const vec_t& v) { vec_t res; res = v; res.Normalize(); return res; } + vec_t Cross(const vec_t& v1, const vec_t& v2) + { + vec_t res; + res.x = v1.y * v2.z - v1.z * v2.y; + res.y = v1.z * v2.x - v1.x * v2.z; + res.z = v1.x * v2.y - v1.y * v2.x; + res.w = 0.f; + return res; + } + + float Dot(const vec_t& v1, const vec_t& v2) + { + return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); + } + + vec_t BuildPlan(const vec_t& p_point1, const vec_t& p_normal) + { + vec_t normal, res; + normal.Normalize(p_normal); + res.w = normal.Dot(p_point1); + res.x = normal.x; + res.y = normal.y; + res.z = normal.z; + return res; + } + + struct matrix_t + { + public: + + union + { + float m[4][4]; + float m16[16]; + struct + { + vec_t right, up, dir, position; + } v; + vec_t component[4]; + }; + + matrix_t(const matrix_t& other) { memcpy(&m16[0], &other.m16[0], sizeof(float) * 16); } + matrix_t() {} + + operator float* () { return m16; } + operator const float* () const { return m16; } + void Translation(float _x, float _y, float _z) { this->Translation(makeVect(_x, _y, _z)); } + + void Translation(const vec_t& vt) + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(vt.x, vt.y, vt.z, 1.f); + } + + void Scale(float _x, float _y, float _z) + { + v.right.Set(_x, 0.f, 0.f, 0.f); + v.up.Set(0.f, _y, 0.f, 0.f); + v.dir.Set(0.f, 0.f, _z, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Scale(const vec_t& s) { Scale(s.x, s.y, s.z); } + + matrix_t& operator *= (const matrix_t& mat) + { + matrix_t tmpMat; + tmpMat = *this; + tmpMat.Multiply(mat); + *this = tmpMat; + return *this; + } + matrix_t operator * (const matrix_t& mat) const + { + matrix_t matT; + matT.Multiply(*this, mat); + return matT; + } + + void Multiply(const matrix_t& matrix) + { + matrix_t tmp; + tmp = *this; + + FPU_MatrixF_x_MatrixF((float*)&tmp, (float*)&matrix, (float*)this); + } + + void Multiply(const matrix_t& m1, const matrix_t& m2) + { + FPU_MatrixF_x_MatrixF((float*)&m1, (float*)&m2, (float*)this); + } + + float GetDeterminant() const + { + return m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] + m[0][2] * m[1][0] * m[2][1] - + m[0][2] * m[1][1] * m[2][0] - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]; + } + + float Inverse(const matrix_t& srcMatrix, bool affine = false); + void SetToIdentity() + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Transpose() + { + matrix_t tmpm; + for (int l = 0; l < 4; l++) + { + for (int c = 0; c < 4; c++) + { + tmpm.m[l][c] = m[c][l]; + } + } + (*this) = tmpm; + } + + void RotationAxis(const vec_t& axis, float angle); + + void OrthoNormalize() + { + v.right.Normalize(); + v.up.Normalize(); + v.dir.Normalize(); + } + }; + + void vec_t::Transform(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + w * matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + w * matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + w * matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + w * matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::Transform(const vec_t& s, const matrix_t& matrix) + { + *this = s; + Transform(matrix); + } + + void vec_t::TransformPoint(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::TransformVector(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + float matrix_t::Inverse(const matrix_t& srcMatrix, bool affine) + { + float det = 0; + + if (affine) + { + det = GetDeterminant(); + float s = 1 / det; + m[0][0] = (srcMatrix.m[1][1] * srcMatrix.m[2][2] - srcMatrix.m[1][2] * srcMatrix.m[2][1]) * s; + m[0][1] = (srcMatrix.m[2][1] * srcMatrix.m[0][2] - srcMatrix.m[2][2] * srcMatrix.m[0][1]) * s; + m[0][2] = (srcMatrix.m[0][1] * srcMatrix.m[1][2] - srcMatrix.m[0][2] * srcMatrix.m[1][1]) * s; + m[1][0] = (srcMatrix.m[1][2] * srcMatrix.m[2][0] - srcMatrix.m[1][0] * srcMatrix.m[2][2]) * s; + m[1][1] = (srcMatrix.m[2][2] * srcMatrix.m[0][0] - srcMatrix.m[2][0] * srcMatrix.m[0][2]) * s; + m[1][2] = (srcMatrix.m[0][2] * srcMatrix.m[1][0] - srcMatrix.m[0][0] * srcMatrix.m[1][2]) * s; + m[2][0] = (srcMatrix.m[1][0] * srcMatrix.m[2][1] - srcMatrix.m[1][1] * srcMatrix.m[2][0]) * s; + m[2][1] = (srcMatrix.m[2][0] * srcMatrix.m[0][1] - srcMatrix.m[2][1] * srcMatrix.m[0][0]) * s; + m[2][2] = (srcMatrix.m[0][0] * srcMatrix.m[1][1] - srcMatrix.m[0][1] * srcMatrix.m[1][0]) * s; + m[3][0] = -(m[0][0] * srcMatrix.m[3][0] + m[1][0] * srcMatrix.m[3][1] + m[2][0] * srcMatrix.m[3][2]); + m[3][1] = -(m[0][1] * srcMatrix.m[3][0] + m[1][1] * srcMatrix.m[3][1] + m[2][1] * srcMatrix.m[3][2]); + m[3][2] = -(m[0][2] * srcMatrix.m[3][0] + m[1][2] * srcMatrix.m[3][1] + m[2][2] * srcMatrix.m[3][2]); + } + else + { + // transpose matrix + float src[16]; + for (int i = 0; i < 4; ++i) + { + src[i] = srcMatrix.m16[i * 4]; + src[i + 4] = srcMatrix.m16[i * 4 + 1]; + src[i + 8] = srcMatrix.m16[i * 4 + 2]; + src[i + 12] = srcMatrix.m16[i * 4 + 3]; + } + + // calculate pairs for first 8 elements (cofactors) + float tmp[12]; // temp array for pairs + tmp[0] = src[10] * src[15]; + tmp[1] = src[11] * src[14]; + tmp[2] = src[9] * src[15]; + tmp[3] = src[11] * src[13]; + tmp[4] = src[9] * src[14]; + tmp[5] = src[10] * src[13]; + tmp[6] = src[8] * src[15]; + tmp[7] = src[11] * src[12]; + tmp[8] = src[8] * src[14]; + tmp[9] = src[10] * src[12]; + tmp[10] = src[8] * src[13]; + tmp[11] = src[9] * src[12]; + + // calculate first 8 elements (cofactors) + m16[0] = (tmp[0] * src[5] + tmp[3] * src[6] + tmp[4] * src[7]) - (tmp[1] * src[5] + tmp[2] * src[6] + tmp[5] * src[7]); + m16[1] = (tmp[1] * src[4] + tmp[6] * src[6] + tmp[9] * src[7]) - (tmp[0] * src[4] + tmp[7] * src[6] + tmp[8] * src[7]); + m16[2] = (tmp[2] * src[4] + tmp[7] * src[5] + tmp[10] * src[7]) - (tmp[3] * src[4] + tmp[6] * src[5] + tmp[11] * src[7]); + m16[3] = (tmp[5] * src[4] + tmp[8] * src[5] + tmp[11] * src[6]) - (tmp[4] * src[4] + tmp[9] * src[5] + tmp[10] * src[6]); + m16[4] = (tmp[1] * src[1] + tmp[2] * src[2] + tmp[5] * src[3]) - (tmp[0] * src[1] + tmp[3] * src[2] + tmp[4] * src[3]); + m16[5] = (tmp[0] * src[0] + tmp[7] * src[2] + tmp[8] * src[3]) - (tmp[1] * src[0] + tmp[6] * src[2] + tmp[9] * src[3]); + m16[6] = (tmp[3] * src[0] + tmp[6] * src[1] + tmp[11] * src[3]) - (tmp[2] * src[0] + tmp[7] * src[1] + tmp[10] * src[3]); + m16[7] = (tmp[4] * src[0] + tmp[9] * src[1] + tmp[10] * src[2]) - (tmp[5] * src[0] + tmp[8] * src[1] + tmp[11] * src[2]); + + // calculate pairs for second 8 elements (cofactors) + tmp[0] = src[2] * src[7]; + tmp[1] = src[3] * src[6]; + tmp[2] = src[1] * src[7]; + tmp[3] = src[3] * src[5]; + tmp[4] = src[1] * src[6]; + tmp[5] = src[2] * src[5]; + tmp[6] = src[0] * src[7]; + tmp[7] = src[3] * src[4]; + tmp[8] = src[0] * src[6]; + tmp[9] = src[2] * src[4]; + tmp[10] = src[0] * src[5]; + tmp[11] = src[1] * src[4]; + + // calculate second 8 elements (cofactors) + m16[8] = (tmp[0] * src[13] + tmp[3] * src[14] + tmp[4] * src[15]) - (tmp[1] * src[13] + tmp[2] * src[14] + tmp[5] * src[15]); + m16[9] = (tmp[1] * src[12] + tmp[6] * src[14] + tmp[9] * src[15]) - (tmp[0] * src[12] + tmp[7] * src[14] + tmp[8] * src[15]); + m16[10] = (tmp[2] * src[12] + tmp[7] * src[13] + tmp[10] * src[15]) - (tmp[3] * src[12] + tmp[6] * src[13] + tmp[11] * src[15]); + m16[11] = (tmp[5] * src[12] + tmp[8] * src[13] + tmp[11] * src[14]) - (tmp[4] * src[12] + tmp[9] * src[13] + tmp[10] * src[14]); + m16[12] = (tmp[2] * src[10] + tmp[5] * src[11] + tmp[1] * src[9]) - (tmp[4] * src[11] + tmp[0] * src[9] + tmp[3] * src[10]); + m16[13] = (tmp[8] * src[11] + tmp[0] * src[8] + tmp[7] * src[10]) - (tmp[6] * src[10] + tmp[9] * src[11] + tmp[1] * src[8]); + m16[14] = (tmp[6] * src[9] + tmp[11] * src[11] + tmp[3] * src[8]) - (tmp[10] * src[11] + tmp[2] * src[8] + tmp[7] * src[9]); + m16[15] = (tmp[10] * src[10] + tmp[4] * src[8] + tmp[9] * src[9]) - (tmp[8] * src[9] + tmp[11] * src[10] + tmp[5] * src[8]); + + // calculate determinant + det = src[0] * m16[0] + src[1] * m16[1] + src[2] * m16[2] + src[3] * m16[3]; + + // calculate matrix inverse + float invdet = 1 / det; + for (int j = 0; j < 16; ++j) + { + m16[j] *= invdet; + } + } + + return det; + } + + void matrix_t::RotationAxis(const vec_t& axis, float angle) + { + float length2 = axis.LengthSq(); + if (length2 < FLT_EPSILON) + { + SetToIdentity(); + return; + } + + vec_t n = axis * (1.f / sqrtf(length2)); + float s = sinf(angle); + float c = cosf(angle); + float k = 1.f - c; + + float xx = n.x * n.x * k + c; + float yy = n.y * n.y * k + c; + float zz = n.z * n.z * k + c; + float xy = n.x * n.y * k; + float yz = n.y * n.z * k; + float zx = n.z * n.x * k; + float xs = n.x * s; + float ys = n.y * s; + float zs = n.z * s; + + m[0][0] = xx; + m[0][1] = xy + zs; + m[0][2] = zx - ys; + m[0][3] = 0.f; + m[1][0] = xy - zs; + m[1][1] = yy; + m[1][2] = yz + xs; + m[1][3] = 0.f; + m[2][0] = zx + ys; + m[2][1] = yz - xs; + m[2][2] = zz; + m[2][3] = 0.f; + m[3][0] = 0.f; + m[3][1] = 0.f; + m[3][2] = 0.f; + m[3][3] = 1.f; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + enum MOVETYPE + { + MT_NONE, + MT_MOVE_X, + MT_MOVE_Y, + MT_MOVE_Z, + MT_MOVE_YZ, + MT_MOVE_ZX, + MT_MOVE_XY, + MT_MOVE_SCREEN, + MT_ROTATE_X, + MT_ROTATE_Y, + MT_ROTATE_Z, + MT_ROTATE_SCREEN, + MT_SCALE_X, + MT_SCALE_Y, + MT_SCALE_Z, + MT_SCALE_XYZ + }; + + static bool IsTranslateType(int type) + { + return type >= MT_MOVE_X && type <= MT_MOVE_SCREEN; + } + + static bool IsRotateType(int type) + { + return type >= MT_ROTATE_X && type <= MT_ROTATE_SCREEN; + } + + static bool IsScaleType(int type) + { + return type >= MT_SCALE_X && type <= MT_SCALE_XYZ; + } + + // Matches MT_MOVE_AB order + static const OPERATION TRANSLATE_PLANS[3] = { TRANSLATE_Y | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Y }; + + struct Context + { + Context() : mbUsing(false), mbEnable(true), mbUsingBounds(false) + { + } + + ImDrawList* mDrawList; + + MODE mMode; + matrix_t mViewMat; + matrix_t mProjectionMat; + matrix_t mModel; + matrix_t mModelInverse; + matrix_t mModelSource; + matrix_t mModelSourceInverse; + matrix_t mMVP; + matrix_t mViewProjection; + + vec_t mModelScaleOrigin; + vec_t mCameraEye; + vec_t mCameraRight; + vec_t mCameraDir; + vec_t mCameraUp; + vec_t mRayOrigin; + vec_t mRayVector; + + float mRadiusSquareCenter; + ImVec2 mScreenSquareCenter; + ImVec2 mScreenSquareMin; + ImVec2 mScreenSquareMax; + + float mScreenFactor; + vec_t mRelativeOrigin; + + bool mbUsing; + bool mbEnable; + + bool mReversed; // reversed projection matrix + + // translation + vec_t mTranslationPlan; + vec_t mTranslationPlanOrigin; + vec_t mMatrixOrigin; + vec_t mTranslationLastDelta; + + // rotation + vec_t mRotationVectorSource; + float mRotationAngle; + float mRotationAngleOrigin; + //vec_t mWorldToLocalAxis; + + // scale + vec_t mScale; + vec_t mScaleValueOrigin; + vec_t mScaleLast; + float mSaveMousePosx; + + // save axis factor when using gizmo + bool mBelowAxisLimit[3]; + bool mBelowPlaneLimit[3]; + float mAxisFactor[3]; + + // bounds stretching + vec_t mBoundsPivot; + vec_t mBoundsAnchor; + vec_t mBoundsPlan; + vec_t mBoundsLocalPivot; + int mBoundsBestAxis; + int mBoundsAxis[2]; + bool mbUsingBounds; + matrix_t mBoundsMatrix; + + // + int mCurrentOperation; + + float mX = 0.f; + float mY = 0.f; + float mWidth = 0.f; + float mHeight = 0.f; + float mXMax = 0.f; + float mYMax = 0.f; + float mDisplayRatio = 1.f; + + bool mIsOrthographic = false; + + int mActualID = -1; + int mEditingID = -1; + OPERATION mOperation = OPERATION(-1); + + bool mAllowAxisFlip = true; + float mGizmoSizeClipSpace = 0.1f; + }; + + static Context gContext; + + static const vec_t directionUnary[3] = { makeVect(1.f, 0.f, 0.f), makeVect(0.f, 1.f, 0.f), makeVect(0.f, 0.f, 1.f) }; + static const ImU32 directionColor[3] = { 0xFF715ED8, 0xFF25AA25, 0xFFCC532C }; + + // Alpha: 100%: FF, 87%: DE, 70%: B3, 54%: 8A, 50%: 80, 38%: 61, 12%: 1F + //static const ImU32 planeColor[3] = { IM_COL32(0xAA, 0, 0, 0x61), IM_COL32(0, 0xAA, 0, 0x61), IM_COL32(0, 0, 0xAA, 0x61) }; + static const ImU32 planeColor[3] = { 0xFF7A68D8, 0xFF55AB55, 0xFFD96742 }; + static const ImU32 selectionColor = 0xFF20AACC; + static const ImU32 inactiveColor = IM_COL32(0x99, 0x99, 0x99, 0x99); + static const ImU32 translationLineColor = IM_COL32(0xAA, 0xAA, 0xAA, 0xAA); + static const char* translationInfoMask[] = { "X : %5.3f", "Y : %5.3f", "Z : %5.3f", + "Y : %5.3f Z : %5.3f", "X : %5.3f Z : %5.3f", "X : %5.3f Y : %5.3f", + "X : %5.3f Y : %5.3f Z : %5.3f" }; + static const char* scaleInfoMask[] = { "X : %5.2f", "Y : %5.2f", "Z : %5.2f", "XYZ : %5.2f" }; + static const char* rotationInfoMask[] = { "X : %5.2f deg %5.2f rad", "Y : %5.2f deg %5.2f rad", "Z : %5.2f deg %5.2f rad", "Screen : %5.2f deg %5.2f rad" }; + static const int translationInfoIndex[] = { 0,0,0, 1,0,0, 2,0,0, 1,2,0, 0,2,0, 0,1,0, 0,1,2 }; + static const float quadMin = 0.5f; + static const float quadMax = 0.8f; + static const float quadUV[8] = { quadMin, quadMin, quadMin, quadMax, quadMax, quadMax, quadMax, quadMin }; + static const int halfCircleSegmentCount = 64; + static const float snapTension = 0.5f; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion); + static int GetRotateType(OPERATION op); + static int GetScaleType(OPERATION op); + + static ImVec2 worldToPos(const vec_t& worldPos, const matrix_t& mat, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + vec_t trans; + trans.TransformPoint(worldPos, mat); + trans *= 0.5f / trans.w; + trans += makeVect(0.5f, 0.5f); + trans.y = 1.f - trans.y; + trans.x *= size.x; + trans.y *= size.y; + trans.x += position.x; + trans.y += position.y; + return ImVec2(trans.x, trans.y); + } + + static void ComputeCameraRay(vec_t& rayOrigin, vec_t& rayDir, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + ImGuiIO& io = ImGui::GetIO(); + + matrix_t mViewProjInverse; + mViewProjInverse.Inverse(gContext.mViewMat * gContext.mProjectionMat); + + const float mox = ((io.MousePos.x - position.x) / size.x) * 2.f - 1.f; + const float moy = (1.f - ((io.MousePos.y - position.y) / size.y)) * 2.f - 1.f; + + const float zNear = gContext.mReversed ? (1.f - FLT_EPSILON) : 0.f; + const float zFar = gContext.mReversed ? 0.f : (1.f - FLT_EPSILON); + + rayOrigin.Transform(makeVect(mox, moy, zNear, 1.f), mViewProjInverse); + rayOrigin *= 1.f / rayOrigin.w; + vec_t rayEnd; + rayEnd.Transform(makeVect(mox, moy, zFar, 1.f), mViewProjInverse); + rayEnd *= 1.f / rayEnd.w; + rayDir = Normalized(rayEnd - rayOrigin); + } + + static float GetSegmentLengthClipSpace(const vec_t& start, const vec_t& end) + { + vec_t startOfSegment = start; + startOfSegment.TransformPoint(gContext.mMVP); + if (fabsf(startOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + startOfSegment *= 1.f / startOfSegment.w; + } + + vec_t endOfSegment = end; + endOfSegment.TransformPoint(gContext.mMVP); + if (fabsf(endOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + endOfSegment *= 1.f / endOfSegment.w; + } + + vec_t clipSpaceAxis = endOfSegment - startOfSegment; + clipSpaceAxis.y /= gContext.mDisplayRatio; + float segmentLengthInClipSpace = sqrtf(clipSpaceAxis.x * clipSpaceAxis.x + clipSpaceAxis.y * clipSpaceAxis.y); + return segmentLengthInClipSpace; + } + + static float GetParallelogram(const vec_t& ptO, const vec_t& ptA, const vec_t& ptB) + { + vec_t pts[] = { ptO, ptA, ptB }; + for (unsigned int i = 0; i < 3; i++) + { + pts[i].TransformPoint(gContext.mMVP); + if (fabsf(pts[i].w) > FLT_EPSILON) // check for axis aligned with camera direction + { + pts[i] *= 1.f / pts[i].w; + } + } + vec_t segA = pts[1] - pts[0]; + vec_t segB = pts[2] - pts[0]; + segA.y /= gContext.mDisplayRatio; + segB.y /= gContext.mDisplayRatio; + vec_t segAOrtho = makeVect(-segA.y, segA.x); + segAOrtho.Normalize(); + float dt = segAOrtho.Dot3(segB); + float surface = sqrtf(segA.x * segA.x + segA.y * segA.y) * fabsf(dt); + return surface; + } + + inline vec_t PointOnSegment(const vec_t& point, const vec_t& vertPos1, const vec_t& vertPos2) + { + vec_t c = point - vertPos1; + vec_t V; + + V.Normalize(vertPos2 - vertPos1); + float d = (vertPos2 - vertPos1).Length(); + float t = V.Dot3(c); + + if (t < 0.f) + { + return vertPos1; + } + + if (t > d) + { + return vertPos2; + } + + return vertPos1 + V * t; + } + + static float IntersectRayPlane(const vec_t& rOrigin, const vec_t& rVector, const vec_t& plan) + { + float numer = plan.Dot3(rOrigin) - plan.w; + float denom = plan.Dot3(rVector); + + if (fabsf(denom) < FLT_EPSILON) // normal is orthogonal to vector, cant intersect + { + return -1.0f; + } + + return -(numer / denom); + } + + static float DistanceToPlane(const vec_t& point, const vec_t& plan) + { + return plan.Dot3(point) + plan.w; + } + + static bool IsInContextRect(ImVec2 p) + { + return IsWithin(p.x, gContext.mX, gContext.mXMax) && IsWithin(p.y, gContext.mY, gContext.mYMax); + } + + void SetRect(float x, float y, float width, float height) + { + gContext.mX = x; + gContext.mY = y; + gContext.mWidth = width; + gContext.mHeight = height; + gContext.mXMax = gContext.mX + gContext.mWidth; + gContext.mYMax = gContext.mY + gContext.mXMax; + gContext.mDisplayRatio = width / height; + } + + void SetOrthographic(bool isOrthographic) + { + gContext.mIsOrthographic = isOrthographic; + } + + void SetDrawlist(ImDrawList* drawlist) + { + gContext.mDrawList = drawlist ? drawlist : ImGui::GetWindowDrawList(); + } + + void SetImGuiContext(ImGuiContext* ctx) + { + ImGui::SetCurrentContext(ctx); + } + + void BeginFrame() + { + const ImU32 flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus; + +#ifdef IMGUI_HAS_VIEWPORT + ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos); +#else + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowSize(io.DisplaySize); + ImGui::SetNextWindowPos(ImVec2(0, 0)); +#endif + + ImGui::PushStyleColor(ImGuiCol_WindowBg, 0); + ImGui::PushStyleColor(ImGuiCol_Border, 0); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + ImGui::Begin("gizmo", NULL, flags); + gContext.mDrawList = ImGui::GetWindowDrawList(); + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + + bool IsUsing() + { + return gContext.mbUsing || gContext.mbUsingBounds; + } + + bool IsOver() + { + return (Intersects(gContext.mOperation, TRANSLATE) && GetMoveType(gContext.mOperation, NULL) != MT_NONE) || + (Intersects(gContext.mOperation, ROTATE) && GetRotateType(gContext.mOperation) != MT_NONE) || + (Intersects(gContext.mOperation, SCALE) && GetScaleType(gContext.mOperation) != MT_NONE) || IsUsing(); + } + + bool IsOver(OPERATION op) + { + if (IsUsing()) + { + return true; + } + if (Intersects(op, SCALE) && GetScaleType(op) != MT_NONE) + { + return true; + } + if (Intersects(op, ROTATE) && GetRotateType(op) != MT_NONE) + { + return true; + } + if (Intersects(op, TRANSLATE) && GetMoveType(op, NULL) != MT_NONE) + { + return true; + } + return false; + } + + void Enable(bool enable) + { + gContext.mbEnable = enable; + if (!enable) + { + gContext.mbUsing = false; + gContext.mbUsingBounds = false; + } + } + + static void ComputeContext(const float* view, const float* projection, float* matrix, MODE mode) + { + gContext.mMode = mode; + gContext.mViewMat = *(matrix_t*)view; + gContext.mProjectionMat = *(matrix_t*)projection; + + if (mode == LOCAL) + { + gContext.mModel = *(matrix_t*)matrix; + gContext.mModel.OrthoNormalize(); + } + else + { + gContext.mModel.Translation(((matrix_t*)matrix)->v.position); + } + gContext.mModelSource = *(matrix_t*)matrix; + gContext.mModelScaleOrigin.Set(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + + gContext.mModelInverse.Inverse(gContext.mModel); + gContext.mModelSourceInverse.Inverse(gContext.mModelSource); + gContext.mViewProjection = gContext.mViewMat * gContext.mProjectionMat; + gContext.mMVP = gContext.mModel * gContext.mViewProjection; + + matrix_t viewInverse; + viewInverse.Inverse(gContext.mViewMat); + gContext.mCameraDir = viewInverse.v.dir; + gContext.mCameraEye = viewInverse.v.position; + gContext.mCameraRight = viewInverse.v.right; + gContext.mCameraUp = viewInverse.v.up; + + // projection reverse + vec_t nearPos, farPos; + nearPos.Transform(makeVect(0, 0, 1.f, 1.f), gContext.mProjectionMat); + farPos.Transform(makeVect(0, 0, 2.f, 1.f), gContext.mProjectionMat); + + gContext.mReversed = (nearPos.z / nearPos.w) > (farPos.z / farPos.w); + + // compute scale from the size of camera right vector projected on screen at the matrix position + vec_t pointRight = viewInverse.v.right; + pointRight.TransformPoint(gContext.mViewProjection); + gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / (pointRight.x / pointRight.w - gContext.mMVP.v.position.x / gContext.mMVP.v.position.w); + + vec_t rightViewInverse = viewInverse.v.right; + rightViewInverse.TransformVector(gContext.mModelInverse); + float rightLength = GetSegmentLengthClipSpace(makeVect(0.f, 0.f), rightViewInverse); + gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / rightLength; + + ImVec2 centerSSpace = worldToPos(makeVect(0.f, 0.f), gContext.mMVP); + gContext.mScreenSquareCenter = centerSSpace; + gContext.mScreenSquareMin = ImVec2(centerSSpace.x - 10.f, centerSSpace.y - 10.f); + gContext.mScreenSquareMax = ImVec2(centerSSpace.x + 10.f, centerSSpace.y + 10.f); + + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector); + } + + static void ComputeColors(ImU32* colors, int type, OPERATION operation) + { + if (gContext.mbEnable) + { + switch (operation) + { + case TRANSLATE: + colors[0] = (type == MT_MOVE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_MOVE_X + i)) ? selectionColor : directionColor[i]; + colors[i + 4] = (type == (int)(MT_MOVE_YZ + i)) ? selectionColor : planeColor[i]; + colors[i + 4] = (type == MT_MOVE_SCREEN) ? selectionColor : colors[i + 4]; + } + break; + case ROTATE: + colors[0] = (type == MT_ROTATE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_ROTATE_X + i)) ? selectionColor : directionColor[i]; + } + break; + case SCALE: + colors[0] = (type == MT_SCALE_XYZ) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_SCALE_X + i)) ? selectionColor : directionColor[i]; + } + break; + // note: this internal function is only called with three possible values for operation + default: + break; + } + } + else + { + for (int i = 0; i < 7; i++) + { + colors[i] = inactiveColor; + } + } + } + + static void ComputeTripodAxisAndVisibility(int axisIndex, vec_t& dirAxis, vec_t& dirPlaneX, vec_t& dirPlaneY, bool& belowAxisLimit, bool& belowPlaneLimit) + { + dirAxis = directionUnary[axisIndex]; + dirPlaneX = directionUnary[(axisIndex + 1) % 3]; + dirPlaneY = directionUnary[(axisIndex + 2) % 3]; + + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID)) + { + // when using, use stored factors so the gizmo doesn't flip when we translate + belowAxisLimit = gContext.mBelowAxisLimit[axisIndex]; + belowPlaneLimit = gContext.mBelowPlaneLimit[axisIndex]; + + dirAxis *= gContext.mAxisFactor[axisIndex]; + dirPlaneX *= gContext.mAxisFactor[(axisIndex + 1) % 3]; + dirPlaneY *= gContext.mAxisFactor[(axisIndex + 2) % 3]; + } + else + { + // new method + float lenDir = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis); + float lenDirMinus = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirAxis); + + float lenDirPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneX); + float lenDirMinusPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneX); + + float lenDirPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneY); + float lenDirMinusPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneY); + + // For readability + bool& allowFlip = gContext.mAllowAxisFlip; + float mulAxis = (allowFlip && lenDir < lenDirMinus && fabsf(lenDir - lenDirMinus) > FLT_EPSILON) ? -1.f : 1.f; + float mulAxisX = (allowFlip && lenDirPlaneX < lenDirMinusPlaneX && fabsf(lenDirPlaneX - lenDirMinusPlaneX) > FLT_EPSILON) ? -1.f : 1.f; + float mulAxisY = (allowFlip && lenDirPlaneY < lenDirMinusPlaneY && fabsf(lenDirPlaneY - lenDirMinusPlaneY) > FLT_EPSILON) ? -1.f : 1.f; + dirAxis *= mulAxis; + dirPlaneX *= mulAxisX; + dirPlaneY *= mulAxisY; + + // for axis + float axisLengthInClipSpace = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis * gContext.mScreenFactor); + + float paraSurf = GetParallelogram(makeVect(0.f, 0.f, 0.f), dirPlaneX * gContext.mScreenFactor, dirPlaneY * gContext.mScreenFactor); + belowPlaneLimit = (paraSurf > 0.0025f); + belowAxisLimit = (axisLengthInClipSpace > 0.02f); + + // and store values + gContext.mAxisFactor[axisIndex] = mulAxis; + gContext.mAxisFactor[(axisIndex + 1) % 3] = mulAxisX; + gContext.mAxisFactor[(axisIndex + 2) % 3] = mulAxisY; + gContext.mBelowAxisLimit[axisIndex] = belowAxisLimit; + gContext.mBelowPlaneLimit[axisIndex] = belowPlaneLimit; + } + } + + static void ComputeSnap(float* value, float snap) + { + if (snap <= FLT_EPSILON) + { + return; + } + + float modulo = fmodf(*value, snap); + float moduloRatio = fabsf(modulo) / snap; + if (moduloRatio < snapTension) + { + *value -= modulo; + } + else if (moduloRatio > (1.f - snapTension)) + { + *value = *value - modulo + snap * ((*value < 0.f) ? -1.f : 1.f); + } + } + static void ComputeSnap(vec_t& value, const float* snap) + { + for (int i = 0; i < 3; i++) + { + ComputeSnap(&value[i], snap[i]); + } + } + + static float ComputeAngleOnPlan() + { + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = Normalized(gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position); + + vec_t perpendicularVector; + perpendicularVector.Cross(gContext.mRotationVectorSource, gContext.mTranslationPlan); + perpendicularVector.Normalize(); + float acosAngle = Clamp(Dot(localPos, gContext.mRotationVectorSource), -1.f, 1.f); + float angle = acosf(acosAngle); + angle *= (Dot(localPos, perpendicularVector) < 0.f) ? 1.f : -1.f; + return angle; + } + + static void DrawRotationGizmo(OPERATION op, int type) + { + if (!Intersects(op, ROTATE)) + { + return; + } + ImDrawList* drawList = gContext.mDrawList; + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, ROTATE); + + vec_t cameraToModelNormalized; + if (gContext.mIsOrthographic) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)&gContext.mViewMat); + cameraToModelNormalized = viewInverse.v.dir; + } + else + { + cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + } + + cameraToModelNormalized.TransformVector(gContext.mModelInverse); + + gContext.mRadiusSquareCenter = screenRotateSize * gContext.mHeight; + + bool hasRSC = Intersects(op, ROTATE_SCREEN); + int circleMul = hasRSC ? 1 : 2; + constexpr float circleLineThickness = 6.0f; + constexpr float lineThickness = 6.0f; + for (int axis = 0; axis < 3; axis++) + { + if (!Intersects(op, static_cast(ROTATE_Z >> axis))) + { + continue; + } + ImVec2* circlePos = (ImVec2*)alloca(sizeof(ImVec2) * (circleMul * halfCircleSegmentCount + 1)); + + float angleStart = atan2f(cameraToModelNormalized[(4 - axis) % 3], cameraToModelNormalized[(3 - axis) % 3]) + ZPI * 0.5f; + + for (int i = 0; i < circleMul * halfCircleSegmentCount + 1; i++) + { + float ng = angleStart + circleMul * ZPI * ((float)i / (float)halfCircleSegmentCount); + vec_t axisPos = makeVect(cosf(ng), sinf(ng), 0.f); + vec_t pos = makeVect(axisPos[axis], axisPos[(axis + 1) % 3], axisPos[(axis + 2) % 3]) * gContext.mScreenFactor; + circlePos[i] = worldToPos(pos, gContext.mMVP); + } + + float radiusAxis = sqrtf((ImLengthSqr(worldToPos(gContext.mModel.v.position, gContext.mViewProjection) - circlePos[0]))); + if (radiusAxis > gContext.mRadiusSquareCenter) + { + gContext.mRadiusSquareCenter = radiusAxis; + } + + drawList->AddPolyline(circlePos, circleMul * halfCircleSegmentCount + 1, colors[3 - axis], false, lineThickness); + } + if (hasRSC) + { + drawList->AddCircle(worldToPos(gContext.mModel.v.position, gContext.mViewProjection), gContext.mRadiusSquareCenter, colors[0], 64, circleLineThickness); + } + + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsRotateType(type)) + { + ImVec2 circlePos[halfCircleSegmentCount + 1]; + + circlePos[0] = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + for (unsigned int i = 1; i < halfCircleSegmentCount; i++) + { + float ng = gContext.mRotationAngle * ((float)(i - 1) / (float)(halfCircleSegmentCount - 1)); + matrix_t rotateVectorMatrix; + rotateVectorMatrix.RotationAxis(gContext.mTranslationPlan, ng); + vec_t pos; + pos.TransformPoint(gContext.mRotationVectorSource, rotateVectorMatrix); + pos *= gContext.mScreenFactor; + circlePos[i] = worldToPos(pos + gContext.mModel.v.position, gContext.mViewProjection); + } + drawList->AddConvexPolyFilled(circlePos, halfCircleSegmentCount, IM_COL32(0xFF, 0x80, 0x10, 0x80)); + drawList->AddPolyline(circlePos, halfCircleSegmentCount, IM_COL32(0xFF, 0x80, 0x10, 0xFF), true, 2); + + ImVec2 destinationPosOnScreen = circlePos[1]; + char tmps[512]; + ImFormatString(tmps, sizeof(tmps), rotationInfoMask[type - MT_ROTATE_X], (gContext.mRotationAngle / ZPI) * 180.f, gContext.mRotationAngle); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps); + } + } + + static void DrawHatchedAxis(const vec_t& axis) + { + for (int j = 1; j < 10; j++) + { + ImVec2 baseSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2) * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2 + 1) * gContext.mScreenFactor, gContext.mMVP); + gContext.mDrawList->AddLine(baseSSpace2, worldDirSSpace2, IM_COL32(0, 0, 0, 0x80), 6.f); + } + } + + static void DrawScaleGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + + if (!Intersects(op, SCALE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, SCALE); + + // draw + vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; + + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID)) + { + scaleDisplay = gContext.mScale; + } + + constexpr float lineThickness = 6.0f; + constexpr float circleSize = 12.0f; + for (unsigned int i = 0; i < 3; i++) + { + if (!Intersects(op, static_cast(SCALE_X << i))) + { + continue; + } + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVP); + + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID)) + { + drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, IM_COL32(0x40, 0x40, 0x40, 0xFF), lineThickness); + drawList->AddCircleFilled(worldDirSSpaceNoScale, circleSize, IM_COL32(0x40, 0x40, 0x40, 0xFF)); + } + + if (!hasTranslateOnAxis || gContext.mbUsing) + { + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], lineThickness); + } + drawList->AddCircleFilled(worldDirSSpace, circleSize, colors[i + 1]); + + //if (gContext.mAxisFactor[i] < 0.f) + //{ + // DrawHatchedAxis(dirAxis * scaleDisplay[i]); + //} + } + } + + // draw screen cirle + drawList->AddCircleFilled(gContext.mScreenSquareCenter, 6.f, colors[0], 32); + + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsScaleType(type)) + { + //ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + */ + char tmps[512]; + //vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_SCALE_X) * 3; + ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps); + } + } + + + static void DrawTranslationGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + if (!drawList) + { + return; + } + + if (!Intersects(op, TRANSLATE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, TRANSLATE); + + const ImVec2 origin = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + + // draw + bool belowAxisLimit = false; + bool belowPlaneLimit = false; + constexpr float arrowSize = 12.0f; + constexpr float lineThickness = 6.0f; + for (unsigned int i = 0; i < 3; ++i) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + + // draw axis + if (belowAxisLimit && Intersects(op, static_cast(TRANSLATE_X << i))) + { + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos(dirAxis * gContext.mScreenFactor, gContext.mMVP); + + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], lineThickness); + + // Arrow head begin + ImVec2 dir(origin - worldDirSSpace); + + float d = sqrtf(ImLengthSqr(dir)); + dir /= d; // Normalize + dir *= arrowSize; + + ImVec2 ortogonalDir(dir.y, -dir.x); // Perpendicular vector + ImVec2 a(worldDirSSpace + dir); + drawList->AddTriangleFilled(worldDirSSpace - dir, a + ortogonalDir, a - ortogonalDir, colors[i + 1]); + // Arrow head end + + //if (gContext.mAxisFactor[i] < 0.f) + //{ + // DrawHatchedAxis(dirAxis); + //} + } + + // draw plane + if (belowPlaneLimit && Contains(op, TRANSLATE_PLANS[i])) + { + ImVec2 screenQuadPts[4]; + for (int j = 0; j < 4; ++j) + { + vec_t cornerWorldPos = (dirPlaneX * quadUV[j * 2] + dirPlaneY * quadUV[j * 2 + 1]) * gContext.mScreenFactor; + screenQuadPts[j] = worldToPos(cornerWorldPos, gContext.mMVP); + } + drawList->AddPolyline(screenQuadPts, 4, directionColor[i], true, 1.0f); + drawList->AddConvexPolyFilled(screenQuadPts, 4, colors[i + 4]); + } + } + + drawList->AddCircleFilled(gContext.mScreenSquareCenter, 6.f, colors[0], 32); + + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsTranslateType(type)) + { + ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + vec_t dif = { destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y, 0.f, 0.f }; + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + + char tmps[512]; + vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_MOVE_X) * 3; + ImFormatString(tmps, sizeof(tmps), translationInfoMask[type - MT_MOVE_X], deltaInfo[translationInfoIndex[componentInfoIndex]], deltaInfo[translationInfoIndex[componentInfoIndex + 1]], deltaInfo[translationInfoIndex[componentInfoIndex + 2]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps); + } + } + + static bool CanActivate() + { + // Check for modifiers + if (StarEngine::Input::IsKeyDown(SE_KEY_LEFT_ALT) || StarEngine::Input::IsKeyDown(SE_KEY_LEFT_SHIFT) || StarEngine::Input::IsKeyDown(SE_KEY_LEFT_CONTROL)) + return false; + + if (ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) + { + return true; + } + return false; + } + + static void HandleAndDrawLocalBounds(const float* bounds, matrix_t* matrix, const float* snapValues, OPERATION operation) + { + ImGuiIO& io = ImGui::GetIO(); + ImDrawList* drawList = gContext.mDrawList; + + // compute best projection axis + vec_t axesWorldDirections[3]; + vec_t bestAxisWorldDirection = { 0.0f, 0.0f, 0.0f, 0.0f }; + int axes[3]; + unsigned int numAxes = 1; + axes[0] = gContext.mBoundsBestAxis; + int bestAxis = axes[0]; + if (!gContext.mbUsingBounds) + { + numAxes = 0; + float bestDot = 0.f; + for (unsigned int i = 0; i < 3; i++) + { + vec_t dirPlaneNormalWorld; + dirPlaneNormalWorld.TransformVector(directionUnary[i], gContext.mModelSource); + dirPlaneNormalWorld.Normalize(); + + float dt = fabsf(Dot(Normalized(gContext.mCameraEye - gContext.mModelSource.v.position), dirPlaneNormalWorld)); + if (dt >= bestDot) + { + bestDot = dt; + bestAxis = i; + bestAxisWorldDirection = dirPlaneNormalWorld; + } + + if (dt >= 0.1f) + { + axes[numAxes] = i; + axesWorldDirections[numAxes] = dirPlaneNormalWorld; + ++numAxes; + } + } + } + + if (numAxes == 0) + { + axes[0] = bestAxis; + axesWorldDirections[0] = bestAxisWorldDirection; + numAxes = 1; + } + + else if (bestAxis != axes[0]) + { + unsigned int bestIndex = 0; + for (unsigned int i = 0; i < numAxes; i++) + { + if (axes[i] == bestAxis) + { + bestIndex = i; + break; + } + } + int tempAxis = axes[0]; + axes[0] = axes[bestIndex]; + axes[bestIndex] = tempAxis; + vec_t tempDirection = axesWorldDirections[0]; + axesWorldDirections[0] = axesWorldDirections[bestIndex]; + axesWorldDirections[bestIndex] = tempDirection; + } + + for (unsigned int axisIndex = 0; axisIndex < numAxes; ++axisIndex) + { + bestAxis = axes[axisIndex]; + bestAxisWorldDirection = axesWorldDirections[axisIndex]; + + // corners + vec_t aabb[4]; + + int secondAxis = (bestAxis + 1) % 3; + int thirdAxis = (bestAxis + 2) % 3; + + for (int i = 0; i < 4; i++) + { + aabb[i][3] = aabb[i][bestAxis] = 0.f; + aabb[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)]; + aabb[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))]; + } + + // draw bounds + unsigned int anchorAlpha = gContext.mbEnable ? IM_COL32_BLACK : IM_COL32(0, 0, 0, 0x80); + + matrix_t boundsMVP = gContext.mModelSource * gContext.mViewProjection; + for (int i = 0; i < 4; i++) + { + ImVec2 worldBound1 = worldToPos(aabb[i], boundsMVP); + ImVec2 worldBound2 = worldToPos(aabb[(i + 1) % 4], boundsMVP); + if (!IsInContextRect(worldBound1) || !IsInContextRect(worldBound2)) + { + continue; + } + float boundDistance = sqrtf(ImLengthSqr(worldBound1 - worldBound2)); + int stepCount = (int)(boundDistance / 10.f); + stepCount = min(stepCount, 1000); + float stepLength = 1.f / (float)stepCount; + for (int j = 0; j < stepCount; j++) + { + float t1 = (float)j * stepLength; + float t2 = (float)j * stepLength + stepLength * 0.5f; + ImVec2 worldBoundSS1 = ImLerp(worldBound1, worldBound2, ImVec2(t1, t1)); + ImVec2 worldBoundSS2 = ImLerp(worldBound1, worldBound2, ImVec2(t2, t2)); + //drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0, 0, 0, 0) + anchorAlpha, 3.f); + drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha, 2.f); + } + vec_t midPoint = (aabb[i] + aabb[(i + 1) % 4]) * 0.5f; + ImVec2 midBound = worldToPos(midPoint, boundsMVP); + static const float AnchorBigRadius = 8.f; + static const float AnchorSmallRadius = 6.f; + bool overBigAnchor = ImLengthSqr(worldBound1 - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + bool overSmallAnchor = ImLengthSqr(midBound - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + + int type = MT_NONE; + vec_t gizmoHitProportion; + + if (Intersects(operation, TRANSLATE)) + { + type = GetMoveType(operation, &gizmoHitProportion); + } + if (Intersects(operation, ROTATE) && type == MT_NONE) + { + type = GetRotateType(operation); + } + if (Intersects(operation, SCALE) && type == MT_NONE) + { + type = GetScaleType(operation); + } + + if (type != MT_NONE) + { + overBigAnchor = false; + overSmallAnchor = false; + } + + unsigned int bigAnchorColor = overBigAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + unsigned int smallAnchorColor = overSmallAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + + drawList->AddCircleFilled(worldBound1, AnchorBigRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(worldBound1, AnchorBigRadius - 1.2f, bigAnchorColor); + + drawList->AddCircleFilled(midBound, AnchorSmallRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(midBound, AnchorSmallRadius - 1.2f, smallAnchorColor); + int oppositeIndex = (i + 2) % 4; + // big anchor on corners + if (!gContext.mbUsingBounds && gContext.mbEnable && overBigAnchor && CanActivate()) + { + gContext.mBoundsPivot.TransformPoint(aabb[(i + 2) % 4], gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(aabb[i], gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + gContext.mBoundsAxis[0] = secondAxis; + gContext.mBoundsAxis[1] = thirdAxis; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[secondAxis] = aabb[oppositeIndex][secondAxis]; + gContext.mBoundsLocalPivot[thirdAxis] = aabb[oppositeIndex][thirdAxis]; + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.mActualID; + gContext.mBoundsMatrix = gContext.mModelSource; + } + // small anchor on middle of segment + if (!gContext.mbUsingBounds && gContext.mbEnable && overSmallAnchor && CanActivate()) + { + vec_t midPointOpposite = (aabb[(i + 2) % 4] + aabb[(i + 3) % 4]) * 0.5f; + gContext.mBoundsPivot.TransformPoint(midPointOpposite, gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(midPoint, gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + int indices[] = { secondAxis , thirdAxis }; + gContext.mBoundsAxis[0] = indices[i % 2]; + gContext.mBoundsAxis[1] = -1; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[gContext.mBoundsAxis[0]] = aabb[oppositeIndex][indices[i % 2]];// bounds[gContext.mBoundsAxis[0]] * (((i + 1) & 2) ? 1.f : -1.f); + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.mActualID; + gContext.mBoundsMatrix = gContext.mModelSource; + } + } + + if (gContext.mbUsingBounds && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID)) + { + matrix_t scale; + scale.SetToIdentity(); + + // compute projected mouse position on plan + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mBoundsPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + + // compute a reference and delta vectors base on mouse move + vec_t deltaVector = (newPos - gContext.mBoundsPivot).Abs(); + vec_t referenceVector = (gContext.mBoundsAnchor - gContext.mBoundsPivot).Abs(); + + // for 1 or 2 axes, compute a ratio that's used for scale and snap it based on resulting length + for (int i = 0; i < 2; i++) + { + int axisIndex1 = gContext.mBoundsAxis[i]; + if (axisIndex1 == -1) + { + continue; + } + + float ratioAxis = 1.f; + vec_t axisDir = gContext.mBoundsMatrix.component[axisIndex1].Abs(); + + float dtAxis = axisDir.Dot(referenceVector); + float boundSize = bounds[axisIndex1 + 3] - bounds[axisIndex1]; + if (dtAxis > FLT_EPSILON) + { + ratioAxis = axisDir.Dot(deltaVector) / dtAxis; + } + + if (snapValues) + { + float length = boundSize * ratioAxis; + ComputeSnap(&length, snapValues[axisIndex1]); + if (boundSize > FLT_EPSILON) + { + ratioAxis = length / boundSize; + } + } + scale.component[axisIndex1] *= ratioAxis; + } + + // transform matrix + matrix_t preScale, postScale; + preScale.Translation(-gContext.mBoundsLocalPivot); + postScale.Translation(gContext.mBoundsLocalPivot); + matrix_t res = preScale * scale * postScale * gContext.mBoundsMatrix; + *matrix = res; + + // info text + char tmps[512]; + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + ImFormatString(tmps, sizeof(tmps), "X: %.2f Y: %.2f Z:%.2f" + , (bounds[3] - bounds[0]) * gContext.mBoundsMatrix.component[0].Length() * scale.component[0].Length() + , (bounds[4] - bounds[1]) * gContext.mBoundsMatrix.component[1].Length() * scale.component[1].Length() + , (bounds[5] - bounds[2]) * gContext.mBoundsMatrix.component[2].Length() * scale.component[2].Length() + ); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), IM_COL32_BLACK, tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), IM_COL32_WHITE, tmps); + } + + if (!io.MouseDown[0]) { + gContext.mbUsingBounds = false; + gContext.mEditingID = -1; + } + if (gContext.mbUsingBounds) + { + break; + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + static int GetScaleType(OPERATION op) + { + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && + Contains(op, SCALE)) + { + type = MT_SCALE_XYZ; + } + + // compute + for (unsigned int i = 0; i < 3 && type == MT_NONE; i++) + { + if (!Intersects(op, static_cast(SCALE_X << i))) + { + continue; + } + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + dirAxis.TransformVector(gContext.mModel); + dirPlaneX.TransformVector(gContext.mModel); + dirPlaneY.TransformVector(gContext.mModel); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; + + const float startOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.0f : 0.1f; + const float endOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.4f : 1.0f; + const ImVec2 posOnPlanScreen = worldToPos(posOnPlan, gContext.mViewProjection); + const ImVec2 axisStartOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * startOffset, gContext.mViewProjection); + const ImVec2 axisEndOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * endOffset, gContext.mViewProjection); + + vec_t closestPointOnAxis = PointOnSegment(makeVect(posOnPlanScreen), makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + + if ((closestPointOnAxis - makeVect(posOnPlanScreen)).Length() < 12.f) // pixel size + { + type = MT_SCALE_X + i; + } + } + return type; + } + + static int GetRotateType(OPERATION op) + { + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; + float dist = deltaScreen.Length(); + if (Intersects(op, ROTATE_SCREEN) && dist >= (gContext.mRadiusSquareCenter - 1.0f) && dist < (gContext.mRadiusSquareCenter + 1.0f)) + { + type = MT_ROTATE_SCREEN; + } + + const vec_t planNormals[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir }; + + vec_t modelViewPos; + modelViewPos.TransformPoint(gContext.mModel.v.position, gContext.mViewMat); + + for (unsigned int i = 0; i < 3 && type == MT_NONE; i++) + { + if (!Intersects(op, static_cast(ROTATE_X << i))) + { + continue; + } + // pickup plan + vec_t pickupPlan = BuildPlan(gContext.mModel.v.position, planNormals[i]); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, pickupPlan); + const vec_t intersectWorldPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t intersectViewPos; + intersectViewPos.TransformPoint(intersectWorldPos, gContext.mViewMat); + + if (ImAbs(modelViewPos.z) - ImAbs(intersectViewPos.z) < -FLT_EPSILON) + { + continue; + } + + const vec_t localPos = intersectWorldPos - gContext.mModel.v.position; + vec_t idealPosOnCircle = Normalized(localPos); + idealPosOnCircle.TransformVector(gContext.mModelInverse); + const ImVec2 idealPosOnCircleScreen = worldToPos(idealPosOnCircle * gContext.mScreenFactor, gContext.mMVP); + + //gContext.mDrawList->AddCircle(idealPosOnCircleScreen, 5.f, IM_COL32_WHITE); + const ImVec2 distanceOnScreen = idealPosOnCircleScreen - io.MousePos; + + const float distance = makeVect(distanceOnScreen).Length(); + if (distance < 8.f) // pixel size + { + type = MT_ROTATE_X + i; + } + } + + return type; + } + + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion) + { + if (!Intersects(op, TRANSLATE)) + { + return MT_NONE; + } + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && + Contains(op, TRANSLATE)) + { + type = MT_MOVE_SCREEN; + } + + const vec_t screenCoord = makeVect(io.MousePos - ImVec2(gContext.mX, gContext.mY)); + + // compute + for (unsigned int i = 0; i < 3 && type == MT_NONE; i++) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + dirAxis.TransformVector(gContext.mModel); + dirPlaneX.TransformVector(gContext.mModel); + dirPlaneY.TransformVector(gContext.mModel); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; + + const ImVec2 axisStartOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * 0.1f, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); + const ImVec2 axisEndOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); + + vec_t closestPointOnAxis = PointOnSegment(screenCoord, makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + if ((closestPointOnAxis - screenCoord).Length() < 12.f && Intersects(op, static_cast(TRANSLATE_X << i))) // pixel size + { + type = MT_MOVE_X + i; + } + + const float dx = dirPlaneX.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + const float dy = dirPlaneY.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + if (belowPlaneLimit && dx >= quadUV[0] && dx <= quadUV[4] && dy >= quadUV[1] && dy <= quadUV[3] && Contains(op, TRANSLATE_PLANS[i])) + { + type = MT_MOVE_YZ + i; + } + + if (gizmoHitProportion) + { + *gizmoHitProportion = makeVect(dx, dy, 0.f); + } + } + return type; + } + + static bool HandleTranslation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if (!Intersects(op, TRANSLATE) || type != MT_NONE) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool applyRotationLocaly = gContext.mMode == LOCAL || type == MT_MOVE_SCREEN; + bool modified = false; + + // move + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation)) + { + ImGui::SetNextFrameWantCaptureMouse(true); + const float len = fabsf(IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan)); // near plan + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + + // compute delta + vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModel.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_MOVE_X && gContext.mCurrentOperation <= MT_MOVE_Z) + { + int axisIndex = gContext.mCurrentOperation - MT_MOVE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex]; + float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; + } + + // snap + if (snap) + { + vec_t cumulativeDelta = gContext.mModel.v.position + delta - gContext.mMatrixOrigin; + if (applyRotationLocaly) + { + matrix_t modelSourceNormalized = gContext.mModelSource; + modelSourceNormalized.OrthoNormalize(); + matrix_t modelSourceNormalizedInverse; + modelSourceNormalizedInverse.Inverse(modelSourceNormalized); + cumulativeDelta.TransformVector(modelSourceNormalizedInverse); + ComputeSnap(cumulativeDelta, snap); + cumulativeDelta.TransformVector(modelSourceNormalized); + } + else + { + ComputeSnap(cumulativeDelta, snap); + } + delta = gContext.mMatrixOrigin + cumulativeDelta - gContext.mModel.v.position; + + } + + if (delta != gContext.mTranslationLastDelta) + { + modified = true; + } + gContext.mTranslationLastDelta = delta; + + // compute matrix & delta + matrix_t deltaMatrixTranslation; + deltaMatrixTranslation.Translation(delta); + if (deltaMatrix) + { + memcpy(deltaMatrix, deltaMatrixTranslation.m16, sizeof(float) * 16); + } + + matrix_t res = gContext.mModelSource * deltaMatrixTranslation; + *(matrix_t*)matrix = res; + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + } + + type = gContext.mCurrentOperation; + } + else + { + // find new possible way to move + vec_t gizmoHitProportion; + type = GetMoveType(op, &gizmoHitProportion); + if (type != MT_NONE) + { + ImGui::SetNextFrameWantCaptureMouse(true); + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.mActualID; + gContext.mCurrentOperation = type; + vec_t movePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, + gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, + -gContext.mCameraDir }; + + vec_t cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + for (unsigned int i = 0; i < 3; i++) + { + vec_t orthoVector = Cross(movePlanNormal[i], cameraToModelNormalized); + movePlanNormal[i].Cross(orthoVector); + movePlanNormal[i].Normalize(); + } + // pickup plan + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_MOVE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModel.v.position; + + gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor); + } + } + return modified; + } + + static bool HandleScale(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if (!Intersects(op, SCALE) || type != MT_NONE) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool modified = false; + + if (!gContext.mbUsing) + { + // find new possible way to scale + type = GetScaleType(op); + if (type != MT_NONE) + { + ImGui::SetNextFrameWantCaptureMouse(true); + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.mActualID; + gContext.mCurrentOperation = type; + const vec_t movePlanNormal[] = { gContext.mModel.v.up, gContext.mModel.v.dir, gContext.mModel.v.right, gContext.mModel.v.dir, gContext.mModel.v.up, gContext.mModel.v.right, -gContext.mCameraDir }; + // pickup plan + + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_SCALE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModel.v.position; + gContext.mScale.Set(1.f, 1.f, 1.f); + gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor); + gContext.mScaleValueOrigin = makeVect(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + gContext.mSaveMousePosx = io.MousePos.x; + } + } + // scale + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation)) + { + ImGui::SetNextFrameWantCaptureMouse(true); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModel.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_SCALE_X && gContext.mCurrentOperation <= MT_SCALE_Z) + { + int axisIndex = gContext.mCurrentOperation - MT_SCALE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex]; + float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; + + vec_t baseVector = gContext.mTranslationPlanOrigin - gContext.mModel.v.position; + float ratio = Dot(axisValue, baseVector + delta) / Dot(axisValue, baseVector); + + gContext.mScale[axisIndex] = max(ratio, 0.001f); + } + else + { + float scaleDelta = (io.MousePos.x - gContext.mSaveMousePosx) * 0.01f; + gContext.mScale.Set(max(1.f + scaleDelta, 0.001f)); + } + + // snap + if (snap) + { + float scaleSnap[] = { snap[0], snap[0], snap[0] }; + ComputeSnap(gContext.mScale, scaleSnap); + } + + // no 0 allowed + for (int i = 0; i < 3; i++) + gContext.mScale[i] = max(gContext.mScale[i], 0.001f); + + if (gContext.mScaleLast != gContext.mScale) + { + modified = true; + } + gContext.mScaleLast = gContext.mScale; + + // compute matrix & delta + matrix_t deltaMatrixScale; + deltaMatrixScale.Scale(gContext.mScale * gContext.mScaleValueOrigin); + + matrix_t res = deltaMatrixScale * gContext.mModel; + *(matrix_t*)matrix = res; + + if (deltaMatrix) + { + vec_t deltaScale = gContext.mScale * gContext.mScaleValueOrigin; + + vec_t originalScaleDivider; + originalScaleDivider.x = 1 / gContext.mModelScaleOrigin.x; + originalScaleDivider.y = 1 / gContext.mModelScaleOrigin.y; + originalScaleDivider.z = 1 / gContext.mModelScaleOrigin.z; + + deltaScale = deltaScale * originalScaleDivider; + + deltaMatrixScale.Scale(deltaScale); + memcpy(deltaMatrix, deltaMatrixScale.m16, sizeof(float) * 16); + } + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mScale.Set(1.f, 1.f, 1.f); + } + + type = gContext.mCurrentOperation; + } + return modified; + } + + static bool HandleRotation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if (!Intersects(op, ROTATE) || type != MT_NONE) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool applyRotationLocaly = gContext.mMode == LOCAL; + bool modified = false; + + if (!gContext.mbUsing) + { + type = GetRotateType(op); + + if (type != MT_NONE) + { + ImGui::SetNextFrameWantCaptureMouse(true); + } + + if (type == MT_ROTATE_SCREEN) + { + applyRotationLocaly = true; + } + + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.mActualID; + gContext.mCurrentOperation = type; + const vec_t rotatePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir }; + // pickup plan + if (applyRotationLocaly) + { + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, rotatePlanNormal[type - MT_ROTATE_X]); + } + else + { + gContext.mTranslationPlan = BuildPlan(gContext.mModelSource.v.position, directionUnary[type - MT_ROTATE_X]); + } + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position; + gContext.mRotationVectorSource = Normalized(localPos); + gContext.mRotationAngleOrigin = ComputeAngleOnPlan(); + } + } + + // rotation + if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation)) + { + ImGui::SetNextFrameWantCaptureMouse(true); + gContext.mRotationAngle = ComputeAngleOnPlan(); + if (snap) + { + float snapInRadian = snap[0] * DEG2RAD; + ComputeSnap(&gContext.mRotationAngle, snapInRadian); + } + vec_t rotationAxisLocalSpace; + + rotationAxisLocalSpace.TransformVector(makeVect(gContext.mTranslationPlan.x, gContext.mTranslationPlan.y, gContext.mTranslationPlan.z, 0.f), gContext.mModelInverse); + rotationAxisLocalSpace.Normalize(); + + matrix_t deltaRotation; + deltaRotation.RotationAxis(rotationAxisLocalSpace, gContext.mRotationAngle - gContext.mRotationAngleOrigin); + if (fabs(gContext.mRotationAngle - gContext.mRotationAngleOrigin) > 0.000001f) + { + modified = true; + } + gContext.mRotationAngleOrigin = gContext.mRotationAngle; + + matrix_t scaleOrigin; + scaleOrigin.Scale(gContext.mModelScaleOrigin); + + if (applyRotationLocaly) + { + *(matrix_t*)matrix = scaleOrigin * deltaRotation * gContext.mModel; + } + else + { + matrix_t res = gContext.mModelSource; + res.v.position.Set(0.f); + + *(matrix_t*)matrix = res * deltaRotation; + ((matrix_t*)matrix)->v.position = gContext.mModelSource.v.position; + } + + if (deltaMatrix) + { + *(matrix_t*)deltaMatrix = gContext.mModelInverse * deltaRotation * gContext.mModel; + } + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mEditingID = -1; + } + type = gContext.mCurrentOperation; + } + return modified; + } + + void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) + { + matrix_t mat = *(matrix_t*)matrix; + + scale[0] = mat.v.right.Length(); + scale[1] = mat.v.up.Length(); + scale[2] = mat.v.dir.Length(); + + mat.OrthoNormalize(); + + rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]); + rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2])); + rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]); + + translation[0] = mat.v.position.x; + translation[1] = mat.v.position.y; + translation[2] = mat.v.position.z; + } + + void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix) + { + matrix_t& mat = *(matrix_t*)matrix; + + matrix_t rot[3]; + for (int i = 0; i < 3; i++) + { + rot[i].RotationAxis(directionUnary[i], rotation[i] * DEG2RAD); + } + + mat = rot[0] * rot[1] * rot[2]; + + float validScale[3]; + for (int i = 0; i < 3; i++) + { + if (fabsf(scale[i]) < FLT_EPSILON) + { + validScale[i] = 0.001f; + } + else + { + validScale[i] = scale[i]; + } + } + mat.v.right *= validScale[0]; + mat.v.up *= validScale[1]; + mat.v.dir *= validScale[2]; + mat.v.position.Set(translation[0], translation[1], translation[2], 1.f); + } + + void SetID(int id) + { + gContext.mActualID = id; + } + + void AllowAxisFlip(bool value) + { + gContext.mAllowAxisFlip = value; + } + + bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix, const float* snap, const float* localBounds, const float* boundsSnap) + { + ComputeContext(view, projection, matrix, mode); + + // set delta to identity + if (deltaMatrix) + { + ((matrix_t*)deltaMatrix)->SetToIdentity(); + } + + // behind camera + vec_t camSpacePosition; + camSpacePosition.TransformPoint(makeVect(0.f, 0.f, 0.f), gContext.mMVP); + if (!gContext.mIsOrthographic && camSpacePosition.z < 0.001f) + { + return false; + } + + // -- + int type = MT_NONE; + bool manipulated = false; + if (gContext.mbEnable) + { + if (!gContext.mbUsingBounds) + { + manipulated = HandleTranslation(matrix, deltaMatrix, operation, type, snap) || + HandleScale(matrix, deltaMatrix, operation, type, snap) || + HandleRotation(matrix, deltaMatrix, operation, type, snap); + } + } + + if (localBounds && !gContext.mbUsing) + { + HandleAndDrawLocalBounds(localBounds, (matrix_t*)matrix, boundsSnap, operation); + } + + gContext.mOperation = operation; + if (!gContext.mbUsingBounds) + { + DrawRotationGizmo(operation, type); + DrawTranslationGizmo(operation, type); + DrawScaleGizmo(operation, type); + } + return manipulated; + } + + void SetGizmoSizeClipSpace(float value) + { + gContext.mGizmoSizeClipSpace = value; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + void ComputeFrustumPlanes(vec_t* frustum, const float* clip) + { + frustum[0].x = clip[3] - clip[0]; + frustum[0].y = clip[7] - clip[4]; + frustum[0].z = clip[11] - clip[8]; + frustum[0].w = clip[15] - clip[12]; + + frustum[1].x = clip[3] + clip[0]; + frustum[1].y = clip[7] + clip[4]; + frustum[1].z = clip[11] + clip[8]; + frustum[1].w = clip[15] + clip[12]; + + frustum[2].x = clip[3] + clip[1]; + frustum[2].y = clip[7] + clip[5]; + frustum[2].z = clip[11] + clip[9]; + frustum[2].w = clip[15] + clip[13]; + + frustum[3].x = clip[3] - clip[1]; + frustum[3].y = clip[7] - clip[5]; + frustum[3].z = clip[11] - clip[9]; + frustum[3].w = clip[15] - clip[13]; + + frustum[4].x = clip[3] - clip[2]; + frustum[4].y = clip[7] - clip[6]; + frustum[4].z = clip[11] - clip[10]; + frustum[4].w = clip[15] - clip[14]; + + frustum[5].x = clip[3] + clip[2]; + frustum[5].y = clip[7] + clip[6]; + frustum[5].z = clip[11] + clip[10]; + frustum[5].w = clip[15] + clip[14]; + + for (int i = 0; i < 6; i++) + { + frustum[i].Normalize(); + } + } + + void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + struct CubeFace + { + float z; + ImVec2 faceCoordsScreen[4]; + ImU32 color; + }; + CubeFace* faces = (CubeFace*)_malloca(sizeof(CubeFace) * matrixCount * 6); + + if (!faces) + { + return; + } + + vec_t frustum[6]; + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + ComputeFrustumPlanes(frustum, viewProjection.m16); + + int cubeFaceCount = 0; + for (int cube = 0; cube < matrixCount; cube++) + { + const float* matrix = &matrices[cube * 16]; + + matrix_t res = *(matrix_t*)matrix * *(matrix_t*)view * *(matrix_t*)projection; + + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + + const vec_t faceCoords[4] = { directionUnary[normalIndex] + directionUnary[perpXIndex] + directionUnary[perpYIndex], + directionUnary[normalIndex] + directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] + directionUnary[perpYIndex], + }; + + // clipping + /* + bool skipFace = false; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + vec_t camSpacePosition; + camSpacePosition.TransformPoint(faceCoords[iCoord] * 0.5f * invert, res); + if (camSpacePosition.z < 0.001f) + { + skipFace = true; + break; + } + } + if (skipFace) + { + continue; + } + */ + vec_t centerPosition, centerPositionVP; + centerPosition.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, *(matrix_t*)matrix); + centerPositionVP.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, res); + + bool inFrustum = true; + for (int iFrustum = 0; iFrustum < 6; iFrustum++) + { + float dist = DistanceToPlane(centerPosition, frustum[iFrustum]); + if (dist < 0.f) + { + inFrustum = false; + break; + } + } + + if (!inFrustum) + { + continue; + } + CubeFace& cubeFace = faces[cubeFaceCount]; + + // 3D->2D + //ImVec2 faceCoordsScreen[4]; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + cubeFace.faceCoordsScreen[iCoord] = worldToPos(faceCoords[iCoord] * 0.5f * invert, res); + } + cubeFace.color = directionColor[normalIndex] | IM_COL32(0x80, 0x80, 0x80, 0); + + cubeFace.z = centerPositionVP.z / centerPositionVP.w; + cubeFaceCount++; + } + } + qsort(faces, cubeFaceCount, sizeof(CubeFace), [](void const* _a, void const* _b) { + CubeFace* a = (CubeFace*)_a; + CubeFace* b = (CubeFace*)_b; + if (a->z < b->z) + { + return 1; + } + return -1; + }); + // draw face with lighter color + for (int iFace = 0; iFace < cubeFaceCount; iFace++) + { + const CubeFace& cubeFace = faces[iFace]; + gContext.mDrawList->AddConvexPolyFilled(cubeFace.faceCoordsScreen, 4, cubeFace.color); + } + + _freea(faces); + } + + void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize) + { + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + vec_t frustum[6]; + ComputeFrustumPlanes(frustum, viewProjection.m16); + matrix_t res = *(matrix_t*)matrix * viewProjection; + + for (float f = -gridSize; f <= gridSize; f += 1.f) + { + for (int dir = 0; dir < 2; dir++) + { + vec_t ptA = makeVect(dir ? -gridSize : f, 0.f, dir ? f : -gridSize); + vec_t ptB = makeVect(dir ? gridSize : f, 0.f, dir ? f : gridSize); + bool visible = true; + for (int i = 0; i < 6; i++) + { + float dA = DistanceToPlane(ptA, frustum[i]); + float dB = DistanceToPlane(ptB, frustum[i]); + if (dA < 0.f && dB < 0.f) + { + visible = false; + break; + } + if (dA > 0.f && dB > 0.f) + { + continue; + } + if (dA < 0.f) + { + float len = fabsf(dA - dB); + float t = fabsf(dA) / len; + ptA.Lerp(ptB, t); + } + if (dB < 0.f) + { + float len = fabsf(dB - dA); + float t = fabsf(dB) / len; + ptB.Lerp(ptA, t); + } + } + if (visible) + { + ImU32 col = IM_COL32(0x80, 0x80, 0x80, 0xFF); + col = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? IM_COL32(0x90, 0x90, 0x90, 0xFF) : col; + col = (fabsf(f) < FLT_EPSILON) ? IM_COL32(0x40, 0x40, 0x40, 0xFF) : col; + + float thickness = 1.f; + thickness = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? 1.5f : thickness; + thickness = (fabsf(f) < FLT_EPSILON) ? 2.3f : thickness; + + gContext.mDrawList->AddLine(worldToPos(ptA, res), worldToPos(ptB, res), col, thickness); + } + } + } + } + + void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) + { + static bool isDraging = false; + static bool isClicking = false; + static bool isInside = false; + static vec_t interpolationUp; + static vec_t interpolationDir; + static int interpolationFrames = 0; + const vec_t referenceUp = makeVect(0.f, 1.f, 0.f); + + matrix_t svgView, svgProjection; + svgView = gContext.mViewMat; + svgProjection = gContext.mProjectionMat; + + ImGuiIO& io = ImGui::GetIO(); + gContext.mDrawList->AddRectFilled(position, position + size, backgroundColor); + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + const vec_t camTarget = viewInverse.v.position - viewInverse.v.dir * length; + + // view/projection matrices + const float distance = 3.f; + matrix_t cubeProjection, cubeView; + float fov = acosf(distance / (sqrtf(distance * distance + 3.f))) * RAD2DEG; + Perspective(fov / sqrtf(2.f), size.x / size.y, 0.01f, 1000.f, cubeProjection.m16); + + vec_t dir = makeVect(viewInverse.m[2][0], viewInverse.m[2][1], viewInverse.m[2][2]); + vec_t up = makeVect(viewInverse.m[1][0], viewInverse.m[1][1], viewInverse.m[1][2]); + vec_t eye = dir * distance; + vec_t zero = makeVect(0.f, 0.f); + LookAt(&eye.x, &zero.x, &up.x, cubeView.m16); + + // set context + gContext.mViewMat = cubeView; + gContext.mProjectionMat = cubeProjection; + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector, position, size); + + const matrix_t res = cubeView * cubeProjection; + + // panels + static const ImVec2 panelPosition[9] = { ImVec2(0.75f,0.75f), ImVec2(0.25f, 0.75f), ImVec2(0.f, 0.75f), + ImVec2(0.75f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.f, 0.25f), + ImVec2(0.75f, 0.f), ImVec2(0.25f, 0.f), ImVec2(0.f, 0.f) }; + + static const ImVec2 panelSize[9] = { ImVec2(0.25f,0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f), + ImVec2(0.25f, 0.5f), ImVec2(0.5f, 0.5f), ImVec2(0.25f, 0.5f), + ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f) }; + + // tag faces + bool boxes[27]{}; + for (int iPass = 0; iPass < 2; iPass++) + { + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + const vec_t indexVectorX = directionUnary[perpXIndex] * invert; + const vec_t indexVectorY = directionUnary[perpYIndex] * invert; + const vec_t boxOrigin = directionUnary[normalIndex] * -invert - indexVectorX - indexVectorY; + + // plan local space + const vec_t n = directionUnary[normalIndex] * invert; + vec_t viewSpaceNormal = n; + vec_t viewSpacePoint = n * 0.5f; + viewSpaceNormal.TransformVector(cubeView); + viewSpaceNormal.Normalize(); + viewSpacePoint.TransformPoint(cubeView); + const vec_t viewSpaceFacePlan = BuildPlan(viewSpacePoint, viewSpaceNormal); + + // back face culling + if (viewSpaceFacePlan.w > 0.f) + { + continue; + } + + const vec_t facePlan = BuildPlan(n * 0.5f, n); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, facePlan); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len - (n * 0.5f); + + float localx = Dot(directionUnary[perpXIndex], posOnPlan) * invert + 0.5f; + float localy = Dot(directionUnary[perpYIndex], posOnPlan) * invert + 0.5f; + + // panels + const vec_t dx = directionUnary[perpXIndex]; + const vec_t dy = directionUnary[perpYIndex]; + const vec_t origin = directionUnary[normalIndex] - dx - dy; + for (int iPanel = 0; iPanel < 9; iPanel++) + { + vec_t boxCoord = boxOrigin + indexVectorX * float(iPanel % 3) + indexVectorY * float(iPanel / 3) + makeVect(1.f, 1.f, 1.f); + const ImVec2 p = panelPosition[iPanel] * 2.f; + const ImVec2 s = panelSize[iPanel] * 2.f; + ImVec2 faceCoordsScreen[4]; + vec_t panelPos[4] = { dx * p.x + dy * p.y, + dx * p.x + dy * (p.y + s.y), + dx * (p.x + s.x) + dy * (p.y + s.y), + dx * (p.x + s.x) + dy * p.y }; + + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + faceCoordsScreen[iCoord] = worldToPos((panelPos[iCoord] + origin) * 0.5f * invert, res, position, size); + } + + const ImVec2 panelCorners[2] = { panelPosition[iPanel], panelPosition[iPanel] + panelSize[iPanel] }; + bool insidePanel = localx > panelCorners[0].x && localx < panelCorners[1].x && localy > panelCorners[0].y && localy < panelCorners[1].y; + int boxCoordInt = int(boxCoord.x * 9.f + boxCoord.y * 3.f + boxCoord.z); + assert(boxCoordInt < 27); + boxes[boxCoordInt] |= insidePanel && (!isDraging); + + // draw face with lighter color + if (iPass) + { + gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, (directionColor[normalIndex] | IM_COL32(0x80, 0x80, 0x80, 0x80)) | (isInside ? IM_COL32(0x08, 0x08, 0x08, 0) : 0)); + if (boxes[boxCoordInt]) + { + gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, IM_COL32(0xF0, 0xA0, 0x60, 0x80)); + + if (!io.MouseDown[0] && !isDraging && isClicking) + { + // apply new view direction + int cx = boxCoordInt / 9; + int cy = (boxCoordInt - cx * 9) / 3; + int cz = boxCoordInt % 3; + interpolationDir = makeVect(1.f - cx, 1.f - cy, 1.f - cz); + interpolationDir.Normalize(); + + if (fabsf(Dot(interpolationDir, referenceUp)) > 1.0f - 0.01f) + { + vec_t right = viewInverse.v.right; + if (fabsf(right.x) > fabsf(right.z)) + { + right.z = 0.f; + } + else + { + right.x = 0.f; + } + right.Normalize(); + interpolationUp = Cross(interpolationDir, right); + interpolationUp.Normalize(); + } + else + { + interpolationUp = referenceUp; + } + interpolationFrames = 40; + isClicking = false; + } + if (io.MouseDown[0] && !isDraging) + { + isClicking = true; + } + } + } + } + } + } + if (interpolationFrames) + { + interpolationFrames--; + vec_t newDir = viewInverse.v.dir; + newDir.Lerp(interpolationDir, 0.2f); + newDir.Normalize(); + + vec_t newUp = viewInverse.v.up; + newUp.Lerp(interpolationUp, 0.3f); + newUp.Normalize(); + newUp = interpolationUp; + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &newUp.x, view); + } + isInside = ImRect(position, position + size).Contains(io.MousePos); + + // drag view + if (!isDraging && io.MouseDown[0] && isInside && (fabsf(io.MouseDelta.x) > 0.f || fabsf(io.MouseDelta.y) > 0.f)) + { + isDraging = true; + isClicking = false; + } + else if (isDraging && !io.MouseDown[0]) + { + isDraging = false; + } + + if (isDraging) + { + matrix_t rx, ry, roll; + + rx.RotationAxis(referenceUp, -io.MouseDelta.x * 0.01f); + ry.RotationAxis(viewInverse.v.right, -io.MouseDelta.y * 0.01f); + + roll = rx * ry; + + vec_t newDir = viewInverse.v.dir; + newDir.TransformVector(roll); + newDir.Normalize(); + + // clamp + vec_t planDir = Cross(viewInverse.v.right, referenceUp); + planDir.y = 0.f; + planDir.Normalize(); + float dt = Dot(planDir, newDir); + if (dt < 0.0f) + { + newDir += planDir * dt; + newDir.Normalize(); + } + + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &referenceUp.x, view); + } + + // restore view/projection because it was used to compute ray + ComputeContext(svgView.m16, svgProjection.m16, gContext.mModelSource.m16, gContext.mMode); + } +}; diff --git a/StarEngine/src/StarEngine/ImGui/ImGuizmo.h b/StarEngine/src/StarEngine/ImGui/ImGuizmo.h new file mode 100644 index 00000000..88b3e050 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/ImGuizmo.h @@ -0,0 +1,215 @@ +// https://github.com/CedricGuillemet/ImGuizmo +// v 1.83 +// +// The MIT License(MIT) +// +// Copyright(c) 2021 Cedric Guillemet +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// ------------------------------------------------------------------------------------------- +// History : +// 2019/11/03 View gizmo +// 2016/09/11 Behind camera culling. Scaling Delta matrix not multiplied by source matrix scales. local/world rotation and translation fixed. Display message is incorrect (X: ... Y:...) in local mode. +// 2016/09/09 Hatched negative axis. Snapping. Documentation update. +// 2016/09/04 Axis switch and translation plan autohiding. Scale transform stability improved +// 2016/09/01 Mogwai changed to Manipulate. Draw debug cube. Fixed inverted scale. Mixing scale and translation/rotation gives bad results. +// 2016/08/31 First version +// +// ------------------------------------------------------------------------------------------- +// Future (no order): +// +// - Multi view +// - display rotation/translation/scale infos in local/world space and not only local +// - finish local/world matrix application +// - OPERATION as bitmask +// +// ------------------------------------------------------------------------------------------- +// Example +#if 0 +void EditTransform(const Camera& camera, matrix_t& matrix) +{ + static ImGuizmo::OPERATION mCurrentGizmoOperation(ImGuizmo::ROTATE); + static ImGuizmo::MODE mCurrentGizmoMode(ImGuizmo::WORLD); + if (ImGui::IsKeyPressed(90)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + if (ImGui::IsKeyPressed(69)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + if (ImGui::IsKeyPressed(82)) // r Key + mCurrentGizmoOperation = ImGuizmo::SCALE; + if (ImGui::RadioButton("Translate", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) + mCurrentGizmoOperation = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) + mCurrentGizmoOperation = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) + mCurrentGizmoOperation = ImGuizmo::SCALE; + float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + ImGuizmo::DecomposeMatrixToComponents(matrix.m16, matrixTranslation, matrixRotation, matrixScale); + ImGui::InputFloat3("Tr", matrixTranslation, 3); + ImGui::InputFloat3("Rt", matrixRotation, 3); + ImGui::InputFloat3("Sc", matrixScale, 3); + ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, matrix.m16); + + if (mCurrentGizmoOperation != ImGuizmo::SCALE) + { + if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) + mCurrentGizmoMode = ImGuizmo::LOCAL; + ImGui::SameLine(); + if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD)) + mCurrentGizmoMode = ImGuizmo::WORLD; + } + static bool useSnap(false); + if (ImGui::IsKeyPressed(83)) + useSnap = !useSnap; + ImGui::Checkbox("", &useSnap); + ImGui::SameLine(); + vec_t snap; + switch (mCurrentGizmoOperation) + { + case ImGuizmo::TRANSLATE: + snap = config.mSnapTranslation; + ImGui::InputFloat3("Snap", &snap.x); + break; + case ImGuizmo::ROTATE: + snap = config.mSnapRotation; + ImGui::InputFloat("Angle Snap", &snap.x); + break; + case ImGuizmo::SCALE: + snap = config.mSnapScale; + ImGui::InputFloat("Scale Snap", &snap.x); + break; + } + ImGuiIO& io = ImGui::GetIO(); + ImGuizmo::SetRect(0, 0, io.DisplaySize.x, io.DisplaySize.y); + ImGuizmo::Manipulate(camera.mView.m16, camera.mProjection.m16, mCurrentGizmoOperation, mCurrentGizmoMode, matrix.m16, NULL, useSnap ? &snap.x : NULL); +} +#endif +#pragma once + +#ifdef USE_IMGUI_API +#include "imconfig.h" +#endif +#ifndef IMGUI_API +#define IMGUI_API +#endif + +#include + +namespace ImGuizmo +{ + // call inside your own window and before Manipulate() in order to draw gizmo to that window. + // Or pass a specific ImDrawList to draw to (e.g. ImGui::GetForegroundDrawList()). + IMGUI_API void SetDrawlist(ImDrawList* drawlist = nullptr); + + // call BeginFrame right after ImGui_XXXX_NewFrame(); + IMGUI_API void BeginFrame(); + + // this is necessary because when imguizmo is compiled into a dll, and imgui into another + // globals are not shared between them. + // More details at https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam + // expose method to set imgui context + IMGUI_API void SetImGuiContext(ImGuiContext* ctx); + + // return true if mouse cursor is over any gizmo control (axis, plan or screen component) + IMGUI_API bool IsOver(); + + // return true if mouse IsOver or if the gizmo is in moving state + IMGUI_API bool IsUsing(); + + // enable/disable the gizmo. Stay in the state until next call to Enable. + // gizmo is rendered with gray half transparent color when disabled + IMGUI_API void Enable(bool enable); + + // helper functions for manualy editing translation/rotation/scale with an input float + // translation, rotation and scale float points to 3 floats each + // Angles are in degrees (more suitable for human editing) + // example: + // float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + // ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale); + // ImGui::InputFloat3("Tr", matrixTranslation, 3); + // ImGui::InputFloat3("Rt", matrixRotation, 3); + // ImGui::InputFloat3("Sc", matrixScale, 3); + // ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16); + // + // These functions have some numerical stability issues for now. Use with caution. + IMGUI_API void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale); + IMGUI_API void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix); + + IMGUI_API void SetRect(float x, float y, float width, float height); + // default is false + IMGUI_API void SetOrthographic(bool isOrthographic); + + // Render a cube with face color corresponding to face normal. Usefull for debug/tests + IMGUI_API void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount); + IMGUI_API void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize); + + // call it when you want a gizmo + // Needs view and projection matrices. + // matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is optional + // translation is applied in world space + enum OPERATION + { + TRANSLATE_X = (1u << 0), + TRANSLATE_Y = (1u << 1), + TRANSLATE_Z = (1u << 2), + ROTATE_X = (1u << 3), + ROTATE_Y = (1u << 4), + ROTATE_Z = (1u << 5), + ROTATE_SCREEN = (1u << 6), + SCALE_X = (1u << 7), + SCALE_Y = (1u << 8), + SCALE_Z = (1u << 9), + BOUNDS = (1u << 10), + TRANSLATE = TRANSLATE_X | TRANSLATE_Y | TRANSLATE_Z, + ROTATE = ROTATE_X | ROTATE_Y | ROTATE_Z | ROTATE_SCREEN, + SCALE = SCALE_X | SCALE_Y | SCALE_Z + }; + + inline OPERATION operator|(OPERATION lhs, OPERATION rhs) + { + return static_cast(static_cast(lhs) | static_cast(rhs)); + } + + enum MODE + { + LOCAL, + WORLD + }; + + IMGUI_API bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix = NULL, const float* snap = NULL, const float* localBounds = NULL, const float* boundsSnap = NULL); + // + // Please note that this cubeview is patented by Autodesk : https://patents.google.com/patent/US7782319B2/en + // It seems to be a defensive patent in the US. I don't think it will bring troubles using it as + // other software are using the same mechanics. But just in case, you are now warned! + // + IMGUI_API void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); + + IMGUI_API void SetID(int id); + + // return true if the cursor is over the operation's gizmo + IMGUI_API bool IsOver(OPERATION op); + IMGUI_API void SetGizmoSizeClipSpace(float value); + + // Allow axis to flip + // When true (default), the guizmo axis flip for better visibility + // When false, they always stay along the positive world/local axis + IMGUI_API void AllowAxisFlip(bool value); +} diff --git a/StarEngine/src/StarEngine/ImGui/PropertyGrid.h b/StarEngine/src/StarEngine/ImGui/PropertyGrid.h new file mode 100644 index 00000000..53734b0e --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/PropertyGrid.h @@ -0,0 +1,2638 @@ +#pragma once + +#include "UICore.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Asset/AssetMetadata.h" +#include "StarEngine/Editor/AssetEditorPanelInterface.h" +#include "StarEngine/ImGui/Colors.h" +#include "StarEngine/ImGui/ImGuiFonts.h" +#include "StarEngine/ImGui/ImGuiUtilities.h" +#include "StarEngine/ImGui/ImGuiWidgets.h" +#include "StarEngine/Scene/Prefab.h" +#include "StarEngine/Scene/Scene.h" +#include "StarEngine/Utilities/StringUtils.h" + +#include +#include +#include + +#include + +#include +#include +#include + +namespace StarEngine { + + class FieldStorage; + +} + +namespace StarEngine::UI { + +#define SE_MESSAGE_BOX_OK_BUTTON BIT(0) +#define SE_MESSAGE_BOX_CANCEL_BUTTON BIT(1) +#define SE_MESSAGE_BOX_USER_FUNC BIT(2) +#define SE_MESSAGE_BOX_AUTO_SIZE BIT(3) + + struct MessageBoxData + { + std::string Title = ""; + std::string Body = ""; + uint32_t Flags = 0; + uint32_t Width = 0; + uint32_t Height = 0; + uint32_t MinWidth = 0; + uint32_t MinHeight = 0; + uint32_t MaxWidth = -1; + uint32_t MaxHeight = -1; + std::function UserRenderFunction; + bool ShouldOpen = true; + bool IsOpen = false; + }; + static std::unordered_map s_MessageBoxes; + + static ImFont* FindFont(const std::string& fontName) + { + return Fonts::Get(fontName); + } + + static void PushFontBold() + { + ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); + } + + static void PushFontLarge() + { + ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]); + } + + template + static void ShowSimpleMessageBox(const char* title, std::format_string message, Args&&... args) + { + auto& messageBoxData = s_MessageBoxes[title]; + messageBoxData.Title = std::format("{0}##MessageBox{1}", title, s_MessageBoxes.size() + 1); + messageBoxData.Body = std::format(message, std::forward(args)...); + messageBoxData.Flags = flags; + messageBoxData.Width = 600; + messageBoxData.Height = 0; + messageBoxData.ShouldOpen = true; + } + + static void ShowMessageBox(const char* title, const std::function& renderFunction, uint32_t width = 600, uint32_t height = 0, uint32_t minWidth = 0, uint32_t minHeight = 0, uint32_t maxWidth = -1, uint32_t maxHeight = -1, uint32_t flags = SE_MESSAGE_BOX_AUTO_SIZE) + { + auto& messageBoxData = s_MessageBoxes[title]; + messageBoxData.Title = std::format("{0}##MessageBox{1}", title, s_MessageBoxes.size() + 1); + messageBoxData.UserRenderFunction = renderFunction; + messageBoxData.Flags = SE_MESSAGE_BOX_USER_FUNC | flags; + messageBoxData.Width = width; + messageBoxData.Height = height; + messageBoxData.MinWidth = minWidth; + messageBoxData.MinHeight = minHeight; + messageBoxData.MaxWidth = maxWidth; + messageBoxData.MaxHeight = maxHeight; + messageBoxData.ShouldOpen = true; + } + + static void RenderMessageBoxes() + { + for (auto& [key, messageBoxData] : s_MessageBoxes) + { + if (messageBoxData.ShouldOpen && !ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId)) + { + ImGui::OpenPopup(messageBoxData.Title.c_str()); + messageBoxData.ShouldOpen = false; + messageBoxData.IsOpen = true; + } + + if (!messageBoxData.IsOpen) + continue; + + if (!ImGui::IsPopupOpen(messageBoxData.Title.c_str())) + { + messageBoxData.IsOpen = false; + continue; + } + + if (messageBoxData.Width != 0 || messageBoxData.Height != 0) + { + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2{ (float)messageBoxData.Width, (float)messageBoxData.Height }, ImGuiCond_Appearing); + } + + ImGuiWindowFlags windowFlags = 0; + if (messageBoxData.Flags & SE_MESSAGE_BOX_AUTO_SIZE) + { + windowFlags |= ImGuiWindowFlags_AlwaysAutoResize; + } + else + { + ImGui::SetNextWindowSizeConstraints(ImVec2{ (float)messageBoxData.MinWidth, (float)messageBoxData.MinHeight }, ImVec2{ (float)messageBoxData.MaxWidth, (float)messageBoxData.MaxHeight }); + } + + if (ImGui::BeginPopupModal(messageBoxData.Title.c_str(), &messageBoxData.IsOpen, windowFlags)) + { + if (messageBoxData.Flags & SE_MESSAGE_BOX_USER_FUNC) + { + SE_CORE_VERIFY(messageBoxData.UserRenderFunction, "No render function provided for message box!"); + messageBoxData.UserRenderFunction(); + } + else + { + ImGui::TextWrapped(messageBoxData.Body.c_str()); + + if (messageBoxData.Flags & SE_MESSAGE_BOX_OK_BUTTON) + { + if (ImGui::Button("Ok")) + ImGui::CloseCurrentPopup(); + + if (messageBoxData.Flags & SE_MESSAGE_BOX_CANCEL_BUTTON) + ImGui::SameLine(); + } + + if (messageBoxData.Flags & SE_MESSAGE_BOX_CANCEL_BUTTON && ImGui::Button("Cancel")) + { + ImGui::CloseCurrentPopup(); + } + } + + ImGui::EndPopup(); + } + } + } + + static bool PropertyGridHeader(const std::string& name, bool openByDefault = true) + { + ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags_Framed + | ImGuiTreeNodeFlags_SpanAvailWidth + | ImGuiTreeNodeFlags_AllowItemOverlap + | ImGuiTreeNodeFlags_FramePadding; + + if (openByDefault) + treeNodeFlags |= ImGuiTreeNodeFlags_DefaultOpen; + + bool open = false; + const float framePaddingX = 6.0f; + const float framePaddingY = 6.0f; // affects height of the header + + UI::ScopedStyle headerRounding(ImGuiStyleVar_FrameRounding, 0.0f); + UI::ScopedStyle headerPaddingAndHeight(ImGuiStyleVar_FramePadding, ImVec2{ framePaddingX, framePaddingY }); + + //UI::PushID(); + ImGui::PushID(name.c_str()); + open = ImGui::TreeNodeEx("##dummy_id", treeNodeFlags, Utils::ToUpper(name).c_str()); + //UI::PopID(); + ImGui::PopID(); + + return open; + } + + static bool TreeNodeWithIcon(const std::string& label, const Ref& icon, const ImVec2& size, bool openByDefault = true) + { + ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags_Framed + | ImGuiTreeNodeFlags_SpanAvailWidth + | ImGuiTreeNodeFlags_AllowItemOverlap + | ImGuiTreeNodeFlags_FramePadding; + + if (openByDefault) + treeNodeFlags |= ImGuiTreeNodeFlags_DefaultOpen; + + bool open = false; + const float framePaddingX = 6.0f; + const float framePaddingY = 6.0f; // affects height of the header + + UI::ScopedStyle headerRounding(ImGuiStyleVar_FrameRounding, 0.0f); + UI::ScopedStyle headerPaddingAndHeight(ImGuiStyleVar_FramePadding, ImVec2{ framePaddingX, framePaddingY }); + + ImGui::PushID(label.c_str()); + ImVec2 contentRegionAvailable = ImGui::GetContentRegionAvail(); + open = ImGui::TreeNodeEx("##dummy_id", treeNodeFlags, ""); + + float lineHeight = ImGui::GetItemRectMax().y - ImGui::GetItemRectMin().y; + ImGui::SameLine(); + UI::ShiftCursorY(size.y / 2.0f - 1.0f); + UI::Image(icon, size); + ImGui::SameLine(); + UI::ShiftCursorY(-(size.y / 2.0f) + 1.0f); + ImGui::TextUnformatted(Utils::ToUpper(label).c_str()); + + ImGui::PopID(); + + return open; + } + + static void Separator() + { + ImGui::Separator(); + } + + static bool PropertyButton(const char* label, const char* buttonText, const char* helpText = "") + { + bool modified = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + modified = ImGui::Button(std::format("{0}##{1}", buttonText, label).c_str()); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, std::string& value, const char* helpText = "") + { + bool modified = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + modified = UI::InputText(std::format("##{0}", label).c_str(), &value); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyMultiline(const char* label, std::string& value, const char* helpText = "") + { + bool modified = false; + + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + + modified = UI::InputTextMultiline(std::format("##{0}", label).c_str(), &value); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return modified; + } + + static void Property(const char* label, const std::string& value, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + BeginDisabled(); + UI::InputText(std::format("##{0}", label).c_str(), (char*)value.c_str(), value.size(), ImGuiInputTextFlags_ReadOnly); + EndDisabled(); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + } + + static void Property(const char* label, const char* value, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + BeginDisabled(); + UI::InputText(std::format("##{0}", label).c_str(), (char*)value, 256, ImGuiInputTextFlags_ReadOnly); + EndDisabled(); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + } + + static bool Property(const char* label, char* value, size_t length, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputText(std::format("##{0}", label).c_str(), value, length); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, bool& value, const char* helpText = "") + { + bool modified = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + modified = UI::Checkbox(std::format("##{0}", label).c_str(), &value); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, int8_t& value, int8_t min = 0, int8_t max = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragInt8(std::format("##{0}", label).c_str(), &value, 1.0f, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, int16_t& value, int16_t min = 0, int16_t max = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragInt16(std::format("##{0}", label).c_str(), &value, 1.0f, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, int32_t& value, int32_t min = 0, int32_t max = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragInt32(std::format("##{0}", label).c_str(), &value, 1.0f, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, int64_t& value, int64_t min = 0, int64_t max = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragInt64(std::format("##{0}", label).c_str(), &value, 1.0f, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, uint8_t& value, uint8_t minValue = 0, uint8_t maxValue = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragUInt8(std::format("##{0}", label).c_str(), &value, 1.0f, minValue, maxValue); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, uint16_t& value, uint16_t minValue = 0, uint16_t maxValue = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragUInt16(std::format("##{0}", label).c_str(), &value, 1.0f, minValue, maxValue); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, uint32_t& value, uint32_t minValue = 0, uint32_t maxValue = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragUInt32(std::format("##{0}", label).c_str(), &value, 1.0f, minValue, maxValue); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, uint64_t& value, uint64_t minValue = 0, uint64_t maxValue = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragUInt64(std::format("##{0}", label).c_str(), &value, 1.0f, minValue, maxValue); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyRadio(const char* label, int& chosen, const std::map& options, const char* helpText = "", const std::map& optionHelpTexts = {}) + { + bool modified = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + + for (auto [value, option] : options) + { + std::string radioLabel = std::format("{}##{}", option.data(), label); + if (ImGui::RadioButton(radioLabel.c_str(), &chosen, value)) + modified = true; + + if (auto optionHelpText = optionHelpTexts.find(value); optionHelpText != optionHelpTexts.end()) + { + if (std::strlen(optionHelpText->second.data()) != 0) + { + ImGui::SameLine(); + HelpMarker(optionHelpText->second.data()); + } + } + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, float& value, float delta = 0.1f, float min = 0.0f, float max = 0.0f, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragFloat(std::format("##{0}", label).c_str(), &value, delta, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, double& value, float delta = 0.1f, double min = 0.0, double max = 0.0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragDouble(std::format("##{0}", label).c_str(), &value, delta, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, glm::vec2& value, float delta = 0.1f, float min = 0.0f, float max = 0.0f, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragFloat2(std::format("##{0}", label).c_str(), glm::value_ptr(value), delta, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, glm::vec3& value, float delta = 0.1f, float min = 0.0f, float max = 0.0f, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + //bool modified = UI::DragFloat3(std::format("##{0}", label).c_str(), glm::value_ptr(value), delta, min, max); + ImVec2 size(ImGui::GetContentRegionAvail().x - 8.0f, ImGui::GetFrameHeightWithSpacing()); + bool manuallyEdited = false; + bool modified = UI::Widgets::EditVec3(std::format("##{0}", label).c_str(), size, 0.0f, manuallyEdited, value, UI::VectorAxis::None, delta, glm::vec3{ min }, glm::vec3{ max }); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, glm::vec4& value, float delta = 0.1f, float min = 0.0f, float max = 0.0f, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::DragFloat4(std::format("##{0}", label).c_str(), glm::value_ptr(value), delta, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool Property(const char* label, glm::bvec3& value, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(" X"); + ImGui::SameLine(); + bool modified = UI::Checkbox(std::format("##1{0}", label).c_str(), &value.x); + ImGui::SameLine(); + ImGui::TextUnformatted(" Y"); + ImGui::SameLine(); + modified |= UI::Checkbox(std::format("##2{0}", label).c_str(), &value.y); + ImGui::SameLine(); + ImGui::TextUnformatted(" Z"); + ImGui::SameLine(); + modified |= UI::Checkbox(std::format("##3{0}", label).c_str(), &value.z); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertySlider(const char* label, int& value, int min, int max, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::SliderInt32(GenerateID(), &value, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertySlider(const char* label, float& value, float min, float max, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::SliderFloat(GenerateID(), &value, min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertySlider(const char* label, glm::vec2& value, float min, float max, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::SliderFloat2(GenerateID(), glm::value_ptr(value), min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertySlider(const char* label, glm::vec3& value, float min, float max, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::SliderFloat3(GenerateID(), glm::value_ptr(value), min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertySlider(const char* label, glm::vec4& value, float min, float max, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::SliderFloat4(GenerateID(), glm::value_ptr(value), min, max); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, int8_t& value, int8_t step = 1, int8_t stepFast = 1, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputInt8(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, int16_t& value, int16_t step = 1, int16_t stepFast = 1, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputInt16(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, int32_t& value, int32_t step = 1, int32_t stepFast = 1, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputInt32(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, int64_t& value, int64_t step = 1, int64_t stepFast = 1, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputInt64(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, uint8_t& value, uint8_t step = 1, uint8_t stepFast = 1, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputUInt8(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, uint16_t& value, uint16_t step = 1, uint16_t stepFast = 1, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputUInt16(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, uint32_t& value, uint32_t step = 1, uint32_t stepFast = 1, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputUInt32(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyInput(const char* label, uint64_t& value, uint64_t step = 0, uint64_t stepFast = 0, ImGuiInputTextFlags flags = 0, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::InputUInt64(GenerateID(), &value, step, stepFast, flags); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyColor(const char* label, glm::vec3& value, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::ColorEdit3(GenerateID(), glm::value_ptr(value)); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyColor(const char* label, glm::vec4& value, const char* helpText = "") + { + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = UI::ColorEdit4(GenerateID(), glm::value_ptr(value)); + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + template + static bool PropertyDropdown(const char* label, const char** options, int32_t optionCount, TEnum& selected, const char* helpText = "") + { + TUnderlying selectedIndex = (TUnderlying)selected; + + const char* current = options[selectedIndex]; + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = false; + + const std::string id = "##" + std::string(label); + if (UI::BeginCombo(id.c_str(), current)) + { + for (int i = 0; i < optionCount; i++) + { + const bool is_selected = (current == options[i]); + if (ImGui::Selectable(options[i], is_selected)) + { + current = options[i]; + selected = (TEnum)i; + modified = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + UI::EndCombo(); + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyDropdown(const char* label, const char** options, int32_t optionCount, int32_t* selected, const char* helpText = "") + { + const char* current = options[*selected]; + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = false; + + const std::string id = "##" + std::string(label); + if (UI::BeginCombo(id.c_str(), current)) + { + for (int i = 0; i < optionCount; i++) + { + const bool is_selected = (current == options[i]); + if (ImGui::Selectable(options[i], is_selected)) + { + current = options[i]; + *selected = i; + modified = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + UI::EndCombo(); + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyDropdownNoLabel(const char* strID, const char** options, int32_t optionCount, int32_t* selected) + { + const char* current = options[*selected]; + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = false; + + const std::string id = "##" + std::string(strID); + if (UI::BeginCombo(id.c_str(), current)) + { + for (int i = 0; i < optionCount; i++) + { + const bool is_selected = (current == options[i]); + if (ImGui::Selectable(options[i], is_selected)) + { + current = options[i]; + *selected = i; + modified = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + UI::EndCombo(); + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + static bool PropertyDropdownNoLabel(const char* strID, const std::vector& options, int32_t* selected, bool advanceColumn = true, bool fullWidth = true) + { + const char* current = options[*selected].c_str(); + ShiftCursorY(4.0f); + + if (fullWidth) + ImGui::PushItemWidth(-1); + + bool modified = false; + + if (IsItemDisabled()) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + current = "---"; + + const std::string id = "##" + std::string(strID); + if (UI::BeginCombo(id.c_str(), current)) + { + for (int i = 0; i < options.size(); i++) + { + const bool is_selected = (current == options[i]); + if (ImGui::Selectable(options[i].c_str(), is_selected)) + { + current = options[i].c_str(); + *selected = i; + modified = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + UI::EndCombo(); + } + + if (IsItemDisabled()) + ImGui::PopStyleVar(); + + if (fullWidth) + ImGui::PopItemWidth(); + + if (advanceColumn) + { + ImGui::NextColumn(); + Draw::Underline(); + } + + return modified; + } + + static bool PropertyDropdown(const char* label, const std::vector& options, int32_t optionCount, int32_t* selected, const char* helpText = "") + { + const char* current = options[*selected].c_str(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + bool modified = false; + + const std::string id = "##" + std::string(label); + if (UI::BeginCombo(id.c_str(), current)) + { + for (int i = 0; i < optionCount; i++) + { + const bool is_selected = (current == options[i]); + if (ImGui::Selectable(options[i].c_str(), is_selected)) + { + current = options[i].c_str(); + *selected = i; + modified = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + UI::EndCombo(); + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return modified; + } + + enum class PropertyAssetReferenceError + { + None = 0, InvalidMetadata + }; + + // TODO(Yan): move this somewhere better when restructuring API + static AssetHandle s_PropertyAssetReferenceAssetHandle; + + struct PropertyAssetReferenceSettings + { + bool AdvanceToNextColumn = true; + bool NoItemSpacing = false; // After label + float WidthOffset = 0.0f; + ImVec4 ButtonLabelColor = ImGui::ColorConvertU32ToFloat4(Colors::Theme::text); + ImVec4 ButtonLabelColorError = ImGui::ColorConvertU32ToFloat4(Colors::Theme::textError); + bool ShowFullFilePath = false; + }; + + inline auto AssetValidityAndName(AssetHandle handle, const PropertyAssetReferenceSettings& settings) + { + std::string name = "Null"; + + bool valid = AssetManager::IsAssetHandleValid(handle); + if (valid) + { + if (settings.ShowFullFilePath) + name = Project::GetEditorAssetManager()->GetMetadata(handle).FilePath.string(); + else + name = Project::GetEditorAssetManager()->GetMetadata(handle).FilePath.stem().string(); + + + if (AssetManager::IsAssetMissing(handle)) + { + valid = false; + name += " (Missing)"; + } +#if WHAT + if (!AssetManager::GetAsset(handle)) + { + if (name.empty()) + { + valid = false; + name = "Invalid Handle"; + } + + if (AssetManager::IsAssetMissing(handle)) + { + valid = false; + name += " (Missing)"; + } + else + { + valid = false; + name += " (Invalid)"; + } + } +#endif + } + + return std::make_pair(valid, name); + } + + template + static bool PropertyAssetReference(const char* label, AssetHandle& outHandle, const char* helpText = "", PropertyAssetReferenceError* outError = nullptr, const PropertyAssetReferenceSettings& settings = {}) + { + bool modified = false; + if (outError) + *outError = PropertyAssetReferenceError::None; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x - settings.WidthOffset; + float itemHeight = 28.0f; + + auto [valid, buttonText] = AssetValidityAndName(outHandle, settings); + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + buttonText = "---"; + + // PropertyAssetReference could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARSP"); + { + UI::ScopedColour buttonLabelColor(ImGuiCol_Text, valid ? settings.ButtonLabelColor : settings.ButtonLabelColorError); + ImGui::Button(GenerateLabelID(buttonText), { width, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + AssetEditorPanelInterface::OpenEditor(AssetManager::GetAsset(outHandle)); + } + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + } + } + + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + bool clear = false; + if (Widgets::AssetSearchPopup(assetSearchPopupID.c_str(), T::GetStaticType(), outHandle, &clear)) + { + if (clear) + outHandle = 0; + modified = true; + s_PropertyAssetReferenceAssetHandle = outHandle; + } + } + + if (!IsItemDisabled()) + { + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + { + AssetHandle assetHandle = *(AssetHandle*)data->Data; + s_PropertyAssetReferenceAssetHandle = assetHandle; + Ref asset = AssetManager::GetAsset(assetHandle); + if (asset && asset->GetAssetType() == T::GetStaticType()) + { + outHandle = assetHandle; + modified = true; + } + } + + ImGui::EndDragDropTarget(); + } + } + + ImGui::PopItemWidth(); + if (settings.AdvanceToNextColumn) + { + ImGui::NextColumn(); + Draw::Underline(); + } + + return modified; + } + + bool PropertyScriptReference(const char* label, UUID& outScriptID, const PropertyAssetReferenceSettings& settings = {}); + + template + static bool PropertyMultiAssetReference(const char* label, AssetHandle& outHandle, const char* helpText = "", PropertyAssetReferenceError* outError = nullptr, const PropertyAssetReferenceSettings& settings = PropertyAssetReferenceSettings()) + { + bool modified = false; + if (outError) + *outError = PropertyAssetReferenceError::None; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + std::initializer_list assetTypes = { TAssetTypes... }; + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x - settings.WidthOffset; + float itemHeight = 28.0f; + + auto [valid, buttonText] = AssetValidityAndName(outHandle, settings); + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + buttonText = "---"; + + // PropertyAssetReference could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARSP"); + + ImGui::PushStyleColor(ImGuiCol_Text, settings.ButtonLabelColor); + ImGui::Button(GenerateLabelID(buttonText), { width, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + AssetEditorPanelInterface::OpenEditor(AssetManager::GetAsset(outHandle)); + } + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + } + ImGui::PopStyleColor(); + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + bool clear = false; + if (Widgets::AssetSearchPopup(assetSearchPopupID.c_str(), outHandle, &clear, "Search Assets", ImVec2{ 250.0f, 350.0f }, assetTypes)) + { + if (clear) + outHandle = 0; + modified = true; + s_PropertyAssetReferenceAssetHandle = outHandle; + } + } + + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + { + AssetHandle assetHandle = *(AssetHandle*)data->Data; + s_PropertyAssetReferenceAssetHandle = assetHandle; + const auto& metadata = Project::GetEditorAssetManager()->GetMetadata(assetHandle); + + for (AssetType type : assetTypes) + { + if (metadata.Type == type) + { + outHandle = assetHandle; + modified = true; + break; + } + } + } + + ImGui::EndDragDropTarget(); + } + + ImGui::PopItemWidth(); + if (settings.AdvanceToNextColumn) + { + ImGui::NextColumn(); + Draw::Underline(); + } + + return modified; + } + + + template + static bool PropertyAssetReferenceWithConversion(const char* label, AssetHandle& outHandle, Fn&& conversionFunc, const char* helpText = "", PropertyAssetReferenceError* outError = nullptr, const PropertyAssetReferenceSettings& settings = {}) + { + bool succeeded = false; + if (outError) + *outError = PropertyAssetReferenceError::None; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x - settings.WidthOffset; + UI::PushID(); + + float itemHeight = 28.0f; + + auto [valid, buttonText] = AssetValidityAndName(outHandle, settings); + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + buttonText = "---"; + + // PropertyAssetReferenceWithConversion could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARWCSP"); + { + UI::ScopedColour buttonLabelColor(ImGuiCol_Text, valid ? settings.ButtonLabelColor : settings.ButtonLabelColorError); + ImGui::Button(GenerateLabelID(buttonText), { width, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + AssetEditorPanelInterface::OpenEditor(AssetManager::GetAsset(outHandle)); + } + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + } + } + + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + AssetHandle assetHandle = outHandle; + + bool clear = false; + if (Widgets::AssetSearchPopup(assetSearchPopupID.c_str(), assetHandle, &clear, "Search Assets", ImVec2(250.0f, 350.0f), { TConversionType::GetStaticType(), TAssetType::GetStaticType() })) + { + if (clear) + { + outHandle = 0; + assetHandle = 0; + succeeded = true; + } + } + UI::PopID(); + + if (!IsItemDisabled()) + { + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + assetHandle = *(AssetHandle*)data->Data; + + ImGui::EndDragDropTarget(); + } + + if (assetHandle != outHandle && assetHandle != 0) + { + s_PropertyAssetReferenceAssetHandle = assetHandle; + + Ref asset = AssetManager::GetAsset(assetHandle); + if (asset) + { + // No conversion necessary + if (asset->GetAssetType() == TAssetType::GetStaticType()) + { + outHandle = assetHandle; + succeeded = true; + } + // Convert + else if (asset->GetAssetType() == TConversionType::GetStaticType()) + { + conversionFunc(asset.As()); + succeeded = false; // Must be handled by conversion function + } + } + else + { + if (outError) + *outError = PropertyAssetReferenceError::InvalidMetadata; + } + } + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + + return succeeded; + } + + static bool PropertyEntityReference(const char* label, UUID& entityID, Ref currentScene, const char* helpText = "") + { + bool receivedValidEntity = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x; + float itemHeight = 28.0f; + + std::string buttonText = "Null"; + + Entity entity = currentScene->TryGetEntityWithUUID(entityID); + if (entity) + buttonText = entity.GetComponent().Tag; + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + buttonText = "---"; + + // PropertyEntityReference could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARSP"); + { + UI::ScopedColour buttonLabelColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Colors::Theme::text)); + ImGui::Button(GenerateLabelID(buttonText), { width, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + if (isHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + bool clear = false; + if (Widgets::EntitySearchPopup(assetSearchPopupID.c_str(), currentScene, entityID, &clear)) + { + if (clear) + entityID = 0; + receivedValidEntity = true; + } + } + + if (!IsItemDisabled()) + { + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("scene_entity_hierarchy"); + if (data) + { + entityID = *(UUID*)data->Data; + receivedValidEntity = true; + } + + ImGui::EndDragDropTarget(); + } + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return receivedValidEntity; + } + + /// + /// Same as PropertyEntityReference, except you can pass in components that the entity is required to have. + /// + /// + /// + /// + /// If true, the entity MUST have all of the components. If false the entity must have only one + /// + template + static bool PropertyEntityReferenceWithComponents(const char* label, UUID& entityID, Ref context, const char* helpText = "", bool requiresAllComponents = true) + { + bool receivedValidEntity = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x; + float itemHeight = 28.0f; + + std::string buttonText = "Null"; + + Entity entity = context->TryGetEntityWithUUID(entityID); + if (entity) + buttonText = entity.GetComponent().Tag; + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + buttonText = "---"; + + ImGui::Button(GenerateLabelID(buttonText), { width, itemHeight }); + } + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("scene_entity_hierarchy"); + if (data) + { + UUID tempID = *(UUID*)data->Data; + Entity temp = context->TryGetEntityWithUUID(tempID); + + if (requiresAllComponents) + { + if (temp.HasComponent()) + { + entityID = tempID; + receivedValidEntity = true; + } + } + else + { + if (temp.HasAny()) + { + entityID = tempID; + receivedValidEntity = true; + } + } + } + + ImGui::EndDragDropTarget(); + } + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return receivedValidEntity; + } + + template + static bool PropertyAssetReferenceTarget(const char* label, const char* assetName, AssetHandle& outHandle, Fn&& targetFunc, const char* helpText = "", const PropertyAssetReferenceSettings& settings = {}) + { + bool modified = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + if (settings.NoItemSpacing) + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 0.0f, 0.0f }); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x - settings.WidthOffset; + UI::PushID(); + + float itemHeight = 28.0f; + + auto [valid, buttonText] = AssetValidityAndName(outHandle, settings); + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + buttonText = "---"; + + // PropertyAssetReferenceTarget could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARTSP"); + { + UI::ScopedColour buttonLabelColor(ImGuiCol_Text, valid ? settings.ButtonLabelColor : settings.ButtonLabelColorError); + ImGui::Button(GenerateLabelID(buttonText), { width, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + AssetEditorPanelInterface::OpenEditor(AssetManager::GetAsset(outHandle)); + } + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + } + } + + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + bool clear = false; + if (Widgets::AssetSearchPopup(assetSearchPopupID.c_str(), T::GetStaticType(), outHandle, &clear)) + { + if (clear) + outHandle = 0; + + targetFunc(AssetManager::GetAsset(outHandle)); + modified = true; + } + + UI::PopID(); + + if (!IsItemDisabled()) + { + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + { + AssetHandle assetHandle = *(AssetHandle*)data->Data; + s_PropertyAssetReferenceAssetHandle = assetHandle; + Ref asset = AssetManager::GetAsset(assetHandle); + if (asset && asset->GetAssetType() == T::GetStaticType()) + { + targetFunc(asset.As()); + modified = true; + } + } + + ImGui::EndDragDropTarget(); + } + } + + ImGui::PopItemWidth(); + if (settings.AdvanceToNextColumn) + { + ImGui::NextColumn(); + Draw::Underline(); + } + if (settings.NoItemSpacing) + ImGui::PopStyleVar(); + return modified; + } + + // To be used in conjunction with AssetSearchPopup. Call ImGui::OpenPopup for AssetSearchPopup if this is clicked. + // Alternatively the click can be used to clear the reference. + template + static bool AssetReferenceDropTargetButton(const char* label, Ref& object, AssetType supportedType, bool& assetDropped, bool dropTargetEnabled = true) + { + bool clicked = false; + + UI::ScopedStyle border(ImGuiStyleVar_FrameBorderSize, 0.0f); + UI::ScopedColourStack colors(ImGuiCol_Button, Colors::Theme::propertyField, + ImGuiCol_ButtonHovered, Colors::Theme::propertyField, + ImGuiCol_ButtonActive, Colors::Theme::propertyField); + + UI::PushID(); + + // For some reason ImGui handles button's width differently + // So need to manually set it if the user has pushed item width + auto* window = ImGui::GetCurrentWindow(); + const bool itemWidthChanged = !window->DC.ItemWidthStack.empty(); + const float buttonWidth = itemWidthChanged ? window->DC.ItemWidth : 0.0f; + + if (object) + { + if (!AssetManager::IsAssetMissing(object->Handle)) + { + UI::ScopedColour text(ImGuiCol_Text, AssetManager::IsAssetValid(object->Handle) ? Colors::Theme::text : Colors::Theme::textError); + auto assetFileName = Project::GetEditorAssetManager()->GetMetadata(object->Handle).FilePath.stem().string(); + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + assetFileName = "---"; + + if (ImGui::Button((char*)assetFileName.c_str(), { buttonWidth, 0.0f })) + clicked = true; + } + else + { + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + { + UI::ScopedColour text(ImGuiCol_Text, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::Button("---", { buttonWidth, 0.0f })) + clicked = true; + } + else + { + UI::ScopedColour text(ImGuiCol_Text, Colors::Theme::textError); + if (ImGui::Button("Missing", { buttonWidth, 0.0f })) + clicked = true; + } + } + } + else + { + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + { + UI::ScopedColour text(ImGuiCol_Text, Colors::Theme::muted); + if (ImGui::Button("---", { buttonWidth, 0.0f })) + clicked = true; + } + else + { + UI::ScopedColour text(ImGuiCol_Text, Colors::Theme::muted); + if (ImGui::Button("Select Asset", { buttonWidth, 0.0f })) + clicked = true; + } + } + + UI::PopID(); + + if (dropTargetEnabled) + { + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + { + AssetHandle assetHandle = *(AssetHandle*)data->Data; + Ref asset = AssetManager::GetAsset(assetHandle); + if (asset && asset->GetAssetType() == supportedType) + { + object = asset.As(); + assetDropped = true; + } + } + + ImGui::EndDragDropTarget(); + } + } + + DrawItemActivityOutline(); + return clicked; + } + + static bool DrawComboPreview(const char* preview, float width = 100.0f) + { + bool pressed = false; + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + ImGui::BeginHorizontal("horizontal_node_layout", ImVec2(width, 0.0f)); + ImGui::PushItemWidth(90.0f); + ImGui::InputText("##selected_asset", (char*)preview, 512, ImGuiInputTextFlags_ReadOnly); + pressed = ImGui::IsItemClicked(); + ImGui::PopItemWidth(); + + ImGui::PushItemWidth(10.0f); + pressed = ImGui::ArrowButton("combo_preview_button", ImGuiDir_Down) || pressed; + ImGui::PopItemWidth(); + + ImGui::EndHorizontal(); + ImGui::PopStyleVar(); + + return pressed; + } + + static int s_CheckboxCount = 0; + + static void BeginCheckboxGroup(const char* label) + { + ImGui::Text(label); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + } + + static bool PropertyCheckboxGroup(const char* label, bool& value) + { + bool modified = false; + + if (++s_CheckboxCount > 1) + ImGui::SameLine(); + + ImGui::Text(label); + ImGui::SameLine(); + return UI::Checkbox(GenerateID(), &value); + } + + static bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)) + { + bool result = ImGui::Button(label, size); + ImGui::NextColumn(); + return result; + } + + static void EndCheckboxGroup() + { + ImGui::PopItemWidth(); + ImGui::NextColumn(); + s_CheckboxCount = 0; + } + + enum class FieldDisableCondition + { + Never, NotWritable, Always + }; + + bool DrawFieldValue(Ref sceneContext, std::string_view fieldName, FieldStorage& storage); + bool DrawFieldArray(Ref sceneContext, std::string_view fieldName, FieldStorage& storage); + +#if 0 + template + static bool PropertyAssetReferenceArray(const char* label, AssetHandle& outHandle, Ref& arrayStorage, uint32_t index, intptr_t& elementToRemove, const char* helpText = "", const PropertyAssetReferenceSettings& settings = {}) + { + bool modified = false; + + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + float itemWidth = ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x)); + ImGui::SetNextItemWidth(itemWidth); + + ImVec2 originalButtonTextAlign = style.ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float itemHeight = 28.0f; + + auto [valid, buttonText] = AssetValidityAndName(outHandle, settings); + + // PropertyAssetReferenceTarget could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARTSP"); + { + UI::ScopedColour buttonLabelColor(ImGuiCol_Text, valid ? settings.ButtonLabelColor : settings.ButtonLabelColorError); + ImGui::Button(GenerateLabelID(buttonText), { itemWidth, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + AssetEditorPanelInterface::OpenEditor(AssetManager::GetAsset(outHandle)); + } + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + } + } + + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + bool clear = false; + if (Widgets::AssetSearchPopup(assetSearchPopupID.c_str(), T::GetStaticType(), outHandle, &clear)) + { + if (clear) + outHandle = 0; + modified = true; + } + } + + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + { + AssetHandle assetHandle = *(AssetHandle*)data->Data; + s_PropertyAssetReferenceAssetHandle = assetHandle; + const auto& metadata = Project::GetEditorAssetManager()->GetMetadata(assetHandle); + + if (metadata.Type == T::GetStaticType()) + { + outHandle = assetHandle; + modified = true; + } + } + + ImGui::EndDragDropTarget(); + } + + if (modified) + arrayStorage->SetValue(static_cast(index), outHandle); + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = intptr_t(index); + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return modified; + } + + static bool PropertyEntityReferenceArray(const char* label, UUID& entityID, Ref context, Ref& arrayStorage, uintptr_t index, intptr_t& elementToRemove) + { + bool receivedValidEntity = false; + + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + float itemWidth = ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x)); + ImGui::SetNextItemWidth(itemWidth); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x; + float itemHeight = 28.0f; + + std::string buttonText = "Null"; + + Entity entity = context->TryGetEntityWithUUID(entityID); + if (entity) + buttonText = entity.GetComponent().Tag; + + ImGui::Button(GenerateLabelID(buttonText), { width - buttonSize, itemHeight }); + } + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("scene_entity_hierarchy"); + if (data) + { + entityID = *(UUID*)data->Data; + receivedValidEntity = true; + } + + ImGui::EndDragDropTarget(); + } + + if (receivedValidEntity) + arrayStorage->SetValue((uint32_t)index, entityID); + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = index; + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return receivedValidEntity; + } + + static bool DrawFieldArray(Ref sceneContext, const std::string& fieldName, Ref arrayStorage) + { + bool modified = false; + intptr_t elementToRemove = -1; + + std::string arrayID = std::format("{0}FieldArray", fieldName); + ImGui::PushID(arrayID.c_str()); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(fieldName.c_str()); + ImGui::NextColumn(); + ImGui::NextColumn(); + + uintptr_t length = arrayStorage->GetLength(); + uintptr_t tempLength = length; + FieldType nativeType = arrayStorage->GetFieldInfo()->Type; + + if (UI::PropertyInput("Length", tempLength, 1, 1, ImGuiInputTextFlags_EnterReturnsTrue)) + { + arrayStorage->Resize((uint32_t)tempLength); + length = tempLength; + modified = true; + } + + ShiftCursorY(5.0f); + + auto drawScalarElement = [&arrayStorage, &elementToRemove, &arrayID](std::string_view indexString, uintptr_t index, ImGuiDataType dataType, auto& data, int32_t components, const char* format) + { + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(indexString.data()); + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + ImGui::SetNextItemWidth(ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x))); + + size_t dataSize = ImGui::DataTypeGetInfo(dataType)->Size; + if (components > 1) + { + if (UI::DragScalarN(GenerateID(), dataType, &data, components, 1.0f, (const void*)0, (const void*)0, format, 0)) + arrayStorage->SetValue>((uint32_t)index, data); + } + else + { + if (UI::DragScalar(GenerateID(), dataType, &data, 1.0f, (const void*)0, (const void*)0, format, (ImGuiSliderFlags)0)) + arrayStorage->SetValue>((uint32_t)index, data); + } + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = index; + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + }; + + auto drawStringElement = [&arrayStorage, &elementToRemove](std::string_view indexString, uintptr_t index, std::string& data) + { + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(indexString.data()); + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + ImGui::SetNextItemWidth(ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x))); + + if (UI::InputText(GenerateID(), &data)) + arrayStorage->SetValue((uint32_t)index, data); + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = index; + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + }; + + for (uint32_t i = 0; i < (uint32_t)length; i++) + { + std::string idString = std::format("[{0}]{1}-{0}", i, arrayID); + std::string indexString = std::format("[{0}]", i); + + ImGui::PushID(idString.c_str()); + + switch (nativeType) + { + case FieldType::Bool: + { + bool value = arrayStorage->GetValue(i); + if (UI::Property(indexString.c_str(), value)) + { + arrayStorage->SetValue(i, value); + modified = true; + } + break; + } + case FieldType::Int8: + { + int8_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S8, value, 1, "%d"); + break; + } + case FieldType::Int16: + { + int16_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S16, value, 1, "%d"); + break; + } + case FieldType::Int32: + { + int32_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S32, value, 1, "%d"); + break; + } + case FieldType::Int64: + { + int64_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S64, value, 1, "%d"); + break; + } + case FieldType::UInt8: + { + uint8_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U8, value, 1, "%d"); + break; + } + case FieldType::UInt16: + { + uint16_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U16, value, 1, "%d"); + break; + } + case FieldType::UInt32: + { + uint32_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U32, value, 1, "%d"); + break; + } + case FieldType::UInt64: + { + uint64_t value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U64, value, 1, "%d"); + break; + } + case FieldType::Float: + { + float value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 1, "%.3f"); + break; + } + case FieldType::Double: + { + double value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Double, value, 1, "%.6f"); + break; + } + case FieldType::String: + { + std::string value = arrayStorage->GetValue(i); + drawStringElement(indexString, i, value); + break; + } + case FieldType::Vector2: + { + glm::vec2 value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 2, "%.3f"); + break; + } + case FieldType::Vector3: + { + glm::vec3 value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 3, "%.3f"); + break; + } + case FieldType::Vector4: + { + glm::vec4 value = arrayStorage->GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 4, "%.3f"); + break; + } + case FieldType::Prefab: + { + AssetHandle handle = arrayStorage->GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, arrayStorage, i, elementToRemove); + break; + } + case FieldType::Entity: + { + UUID uuid = arrayStorage->GetValue(i); + PropertyEntityReferenceArray(indexString.c_str(), uuid, sceneContext, arrayStorage, i, elementToRemove); + break; + } + case FieldType::Mesh: + { + AssetHandle handle = arrayStorage->GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, arrayStorage, i, elementToRemove); + break; + } + case FieldType::StaticMesh: + { + AssetHandle handle = arrayStorage->GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, arrayStorage, i, elementToRemove); + break; + } + case FieldType::Material: + { + AssetHandle handle = arrayStorage->GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, arrayStorage, i, elementToRemove); + break; + } + /*case UnmanagedType::PhysicsMaterial: + { + ColliderMaterial material = storage->GetValue(managedInstance); + + if (PropertyGridHeader(std::format("{} (ColliderMaterial)", fieldName))) + { + if (Property("Friction", material.Friction)) + { + storage->SetValue(managedInstance, material); + result = true; + } + + if (Property("Restitution", material.Restitution)) + { + storage->SetValue(managedInstance, material); + result = true; + } + + EndTreeNode(); + } + + AssetHandle handle = valueWrapper.GetOrDefault(0); + PropertyAssetReferenceArray(indexString.c_str(), handle, managedInstance, arrayStorage, i, elementToRemove); + break; + }*/ + case FieldType::Texture2D: + { + AssetHandle handle = arrayStorage->GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, arrayStorage, i, elementToRemove); + break; + } + case FieldType::Scene: + { + AssetHandle handle = arrayStorage->GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, arrayStorage, i, elementToRemove); + break; + } + } + + ImGui::PopID(); + } + + ImGui::PopID(); + + if (elementToRemove != -1) + { + arrayStorage->RemoveAt((uint32_t)elementToRemove); + modified = true; + } + + return modified; + } +#endif +} diff --git a/StarEngine/src/StarEngine/ImGui/StarImGui.cpp b/StarEngine/src/StarEngine/ImGui/StarImGui.cpp new file mode 100644 index 00000000..18bc52f8 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/StarImGui.cpp @@ -0,0 +1,839 @@ +#include "sepch.h" + +#include "PropertyGrid.h" + +#include "StarEngine/Scripting/ScriptEngine.h" + +namespace StarEngine::UI { + + bool PropertyScriptReference(const char* label, UUID& outScriptID, const PropertyAssetReferenceSettings& settings) + { + bool modified = false; + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const auto& scriptEngine = ScriptEngine::GetInstance(); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x - settings.WidthOffset; + float itemHeight = 28.0f; + + std::string buttonText = "None"; + bool valid = true; + if (scriptEngine.IsValidScript(outScriptID)) + { + const auto& metadata = scriptEngine.GetScriptMetadata(outScriptID); + buttonText = metadata.FullName; + } + + if ((GImGui->CurrentItemFlags & ImGuiItemFlags_MixedValue) != 0) + buttonText = "---"; + + // PropertyAssetReference could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARSP"); + { + UI::ScopedColour buttonLabelColor(ImGuiCol_Text, valid ? settings.ButtonLabelColor : settings.ButtonLabelColorError); + ImGui::Button(GenerateLabelID(buttonText), { width, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + // TODO(Peter): Open script in default editor + } + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + } + } + + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + bool clear = false; + if (Widgets::ScriptSearchPopup(assetSearchPopupID.c_str(), scriptEngine, outScriptID, &clear)) + { + if (clear) + outScriptID = 0; + modified = true; + //s_PropertyAssetReferenceAssetHandle = outScriptID; + } + } + + /*if (!IsItemDisabled()) + { + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + { + AssetHandle assetHandle = *(AssetHandle*)data->Data; + s_PropertyAssetReferenceAssetHandle = assetHandle; + Ref asset = AssetManager::GetAsset(assetHandle); + if (asset && asset->GetAssetType() == T::GetStaticType()) + { + outHandle = assetHandle; + modified = true; + } + } + + ImGui::EndDragDropTarget(); + } + }*/ + + ImGui::PopItemWidth(); + if (settings.AdvanceToNextColumn) + { + ImGui::NextColumn(); + Draw::Underline(); + } + + return modified; + } + + bool DrawFieldValue(Ref sceneContext, std::string_view fieldName, FieldStorage& storage) + { + ImGui::PushID(fieldName.data()); + + bool result = false; + + switch (storage.GetType()) + { + case DataType::Bool: + { + bool value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::SByte: + { + int8_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Short: + { + int16_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Int: + { + int32_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Long: + { + int64_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Byte: + { + uint8_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::UShort: + { + uint16_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::UInt: + { + uint32_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::ULong: + { + uint64_t value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Float: + { + float value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Double: + { + double value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::String: + { + std::string value = storage.GetValue(); + + // TODO(Emily): `Property` looks like it expects a C string but we're passing string_view data which may + // Not be null-terminated or may not be intended to read to terminator. + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Vector2: + { + glm::vec2 value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Vector3: + { + glm::vec3 value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Vector4: + { + glm::vec4 value = storage.GetValue(); + if (Property(fieldName.data(), value)) + { + storage.SetValue(value); + result = true; + } + break; + } + case DataType::Prefab: + { + /*if (sceneContext->IsPlaying()) + { + Coral::ManagedObject prefabObj = storage.GetValue(); + AssetHandle handle = prefabObj.GetFieldValue("Handle"); + if (PropertyAssetReference(fieldName.data(), handle)) + { + prefabObj.SetFieldValue("Handle", (uint64_t)handle); + result = true; + } + } + else*/ + if (!sceneContext->IsPlaying()) + { + AssetHandle handle = storage.GetValue(); + if (PropertyAssetReference(fieldName.data(), handle)) + { + storage.SetValue(handle); + result = true; + } + } + + break; + } + case DataType::Entity: + { + /*if (sceneContext->IsPlaying()) + { + Coral::ManagedObject entityObj = storage.GetValue(); + UUID uuid = entityObj.GetFieldValue("ID"); + if (PropertyEntityReference(fieldName.data(), uuid, sceneContext)) + { + entityObj.SetFieldValue("ID", (uint64_t)uuid); + result = true; + } + } + else*/ + if (!sceneContext->IsPlaying()) + { + UUID uuid = storage.GetValue(); + if (PropertyEntityReference(fieldName.data(), uuid, sceneContext)) + { + storage.SetValue(uuid); + result = true; + } + } + + break; + } + case DataType::Mesh: + { + if (!sceneContext->IsPlaying()) + { + AssetHandle handle = storage.GetValue(); + if (PropertyAssetReference(fieldName.data(), handle)) + { + storage.SetValue(handle); + result = true; + } + } + break; + } + case DataType::StaticMesh: + { + if (!sceneContext->IsPlaying()) + { + AssetHandle handle = storage.GetValue(); + if (PropertyAssetReference(fieldName.data(), handle)) + { + storage.SetValue(handle); + result = true; + } + } + break; + } + case DataType::Material: + { + if (!sceneContext->IsPlaying()) + { + AssetHandle handle = storage.GetValue(); + if (PropertyAssetReference(fieldName.data(), handle)) + { + storage.SetValue(handle); + result = true; + } + } + break; + } + case DataType::Texture2D: + { + AssetHandle handle = storage.GetValue(); + if (PropertyAssetReference(fieldName.data(), handle)) + storage.SetValue(handle); + break; + } + case DataType::Scene: + { + if (!sceneContext->IsPlaying()) + { + AssetHandle handle = storage.GetValue(); + if (PropertyAssetReference(fieldName.data(), handle)) + storage.SetValue(handle); + } + break; + } + } + + ImGui::PopID(); + + return result; + } + + template + static bool PropertyAssetReferenceArray(const char* label, AssetHandle& outHandle, FieldStorage& arrayStorage, uint32_t index, intptr_t& elementToRemove, const char* helpText = "", const PropertyAssetReferenceSettings& settings = {}) + { + bool modified = false; + + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + + if (std::strlen(helpText) != 0) + { + ImGui::SameLine(); + HelpMarker(helpText); + } + + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + float itemWidth = ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x)); + ImGui::SetNextItemWidth(itemWidth); + + ImVec2 originalButtonTextAlign = style.ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float itemHeight = 28.0f; + + std::string buttonText = "Null"; + bool valid = AssetManager::IsAssetValid(outHandle) && !AssetManager::IsAssetMissing(outHandle); + + if (valid) + { + buttonText = Project::GetEditorAssetManager()->GetMetadata(outHandle).FilePath.stem().string(); + } + + // PropertyAssetReferenceTarget could be called multiple times in same "context" + // and so we need a unique id for the asset search popup each time. + // notes + // - don't use GenerateID(), that's inviting id clashes, which would be super confusing. + // - don't store return from GenerateLabelId in a const char* here. Because its pointing to an internal + // buffer which may get overwritten by the time you want to use it later on. + std::string assetSearchPopupID = GenerateLabelID("ARTSP"); + { + UI::ScopedColour buttonLabelColor(ImGuiCol_Text, valid ? settings.ButtonLabelColor : settings.ButtonLabelColorError); + ImGui::Button(GenerateLabelID(buttonText), { itemWidth, itemHeight }); + + const bool isHovered = ImGui::IsItemHovered(); + + if (isHovered) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + // NOTE(Peter): Ugly workaround since AssetEditorPanel includes ImGui.h (meaning ImGui.h can't include AssetEditorPanel). + // Will rework those includes at a later date... + AssetEditorPanelInterface::OpenEditor(AssetManager::GetAsset(outHandle)); + } + else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + ImGui::OpenPopup(assetSearchPopupID.c_str()); + } + } + } + + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + bool clear = false; + if (Widgets::AssetSearchPopup(assetSearchPopupID.c_str(), T::GetStaticType(), outHandle, &clear)) + { + if (clear) + outHandle = 0; + modified = true; + } + } + + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("asset_payload"); + + if (data) + { + AssetHandle assetHandle = *(AssetHandle*)data->Data; + s_PropertyAssetReferenceAssetHandle = assetHandle; + const auto& metadata = Project::GetEditorAssetManager()->GetMetadata(assetHandle); + + if (metadata.Type == T::GetStaticType()) + { + outHandle = assetHandle; + modified = true; + } + } + + ImGui::EndDragDropTarget(); + } + + if (modified) + arrayStorage.SetValue(outHandle, static_cast(index)); + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = intptr_t(index); + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return modified; + } + + static bool PropertyEntityReferenceArray(const char* label, UUID& entityID, Ref context, FieldStorage& arrayStorage, uintptr_t index, intptr_t& elementToRemove) + { + bool receivedValidEntity = false; + + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(label); + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + float itemWidth = ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x)); + ImGui::SetNextItemWidth(itemWidth); + + ImVec2 originalButtonTextAlign = ImGui::GetStyle().ButtonTextAlign; + { + ImGui::GetStyle().ButtonTextAlign = { 0.0f, 0.5f }; + float width = ImGui::GetContentRegionAvail().x; + float itemHeight = 28.0f; + + std::string buttonText = "Null"; + + Entity entity = context->TryGetEntityWithUUID(entityID); + if (entity) + buttonText = entity.GetComponent().Tag; + + ImGui::Button(GenerateLabelID(buttonText), { width - buttonSize, itemHeight }); + } + ImGui::GetStyle().ButtonTextAlign = originalButtonTextAlign; + + if (ImGui::BeginDragDropTarget()) + { + auto data = ImGui::AcceptDragDropPayload("scene_entity_hierarchy"); + if (data) + { + entityID = *(UUID*)data->Data; + receivedValidEntity = true; + } + + ImGui::EndDragDropTarget(); + } + + if (receivedValidEntity) + arrayStorage.SetValue(entityID, (uint64_t)index); + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = index; + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + + return receivedValidEntity; + } + + bool DrawFieldArray(Ref sceneContext, std::string_view fieldName, FieldStorage& storage) + { + bool modified = false; + intptr_t elementToRemove = -1; + + std::string arrayID = std::format("{0}FieldArray", fieldName); + ImGui::PushID(arrayID.c_str()); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(fieldName.data()); + ImGui::NextColumn(); + ImGui::NextColumn(); + + int32_t length = storage.GetLength(); + + if (UI::PropertyInput("Length", length, 1, 1, ImGuiInputTextFlags_EnterReturnsTrue)) + { + storage.Resize(length); + modified = true; + + length = storage.GetLength(); + } + + ShiftCursorY(5.0f); + + auto drawScalarElement = [&storage, &elementToRemove, &arrayID](std::string_view indexString, uintptr_t index, ImGuiDataType dataType, auto& data, int32_t components, const char* format) + { + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(indexString.data()); + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + ImGui::SetNextItemWidth(ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x))); + + size_t dataSize = ImGui::DataTypeGetInfo(dataType)->Size; + if (components > 1) + { + if (UI::DragScalarN(GenerateID(), dataType, &data, components, 1.0f, (const void*)0, (const void*)0, format, 0)) + storage.SetValue>(data, index); + } + else + { + if (UI::DragScalar(GenerateID(), dataType, &data, 1.0f, (const void*)0, (const void*)0, format, (ImGuiSliderFlags)0)) + storage.SetValue>(data, index); + } + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = index; + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + }; + + auto drawStringElement = [&storage, &elementToRemove](std::string_view indexString, uintptr_t index, std::string& data) + { + ImGuiStyle& style = ImGui::GetStyle(); + + ShiftCursor(10.0f, 9.0f); + ImGui::Text(indexString.data()); + ImGui::NextColumn(); + ShiftCursorY(4.0f); + ImGui::PushItemWidth(-1); + + const float buttonSize = ImGui::GetFrameHeight(); + ImGui::SetNextItemWidth(ImMax(1.0f, ImGui::CalcItemWidth() - (buttonSize + style.ItemInnerSpacing.x))); + + if (UI::InputText(GenerateID(), &data)) + storage.SetValue(data, index); + + const ImVec2 backupFramePadding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGui::SameLine(0, style.ItemInnerSpacing.x); + + if (ImGui::Button(GenerateLabelID("x"), ImVec2(buttonSize, buttonSize))) + elementToRemove = index; + + style.FramePadding = backupFramePadding; + + ImGui::PopItemWidth(); + ImGui::NextColumn(); + Draw::Underline(); + }; + + for (int32_t i = 0; i < length; i++) + { + std::string idString = std::format("[{0}]{1}-{0}", i, arrayID); + std::string indexString = std::format("[{0}]", i); + + ImGui::PushID(idString.c_str()); + + switch (storage.GetType()) + { + case DataType::Bool: + { + bool value = storage.GetValue(i); + if (UI::Property(indexString.c_str(), value)) + { + storage.SetValue(value, i); + modified = true; + } + break; + } + case DataType::SByte: + { + int8_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S8, value, 1, "%d"); + break; + } + case DataType::Short: + { + int16_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S16, value, 1, "%d"); + break; + } + case DataType::Int: + { + int32_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S32, value, 1, "%d"); + break; + } + case DataType::Long: + { + int64_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_S64, value, 1, "%d"); + break; + } + case DataType::Byte: + { + uint8_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U8, value, 1, "%d"); + break; + } + case DataType::UShort: + { + uint16_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U16, value, 1, "%d"); + break; + } + case DataType::UInt: + { + uint32_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U32, value, 1, "%d"); + break; + } + case DataType::ULong: + { + uint64_t value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_U64, value, 1, "%d"); + break; + } + case DataType::Float: + { + float value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 1, "%.3f"); + break; + } + case DataType::Double: + { + double value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Double, value, 1, "%.6f"); + break; + } + case DataType::String: + { + std::string value = storage.GetValue(i); + drawStringElement(indexString, i, value); + break; + } + case DataType::Vector2: + { + glm::vec2 value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 2, "%.3f"); + break; + } + case DataType::Vector3: + { + glm::vec3 value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 3, "%.3f"); + break; + } + case DataType::Vector4: + { + glm::vec4 value = storage.GetValue(i); + drawScalarElement(indexString, i, ImGuiDataType_Float, value, 4, "%.3f"); + break; + } + case DataType::Prefab: + { + AssetHandle handle = storage.GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, storage, i, elementToRemove); + break; + } + case DataType::Entity: + { + UUID uuid = storage.GetValue(i); + PropertyEntityReferenceArray(indexString.c_str(), uuid, sceneContext, storage, i, elementToRemove); + break; + } + case DataType::Mesh: + { + AssetHandle handle = storage.GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, storage, i, elementToRemove); + break; + } + case DataType::StaticMesh: + { + AssetHandle handle = storage.GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, storage, i, elementToRemove); + break; + } + case DataType::Material: + { + AssetHandle handle = storage.GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, storage, i, elementToRemove); + break; + } + /*case UnmanagedType::PhysicsMaterial: + { + ColliderMaterial material = storage->GetValue(managedInstance); + + if (PropertyGridHeader(fmt::format("{} (ColliderMaterial)", fieldName))) + { + if (Property("Friction", material.Friction)) + { + storage->SetValue(managedInstance, material); + result = true; + } + + if (Property("Restitution", material.Restitution)) + { + storage->SetValue(managedInstance, material); + result = true; + } + + EndTreeNode(); + } + + AssetHandle handle = valueWrapper.GetOrDefault(0); + PropertyAssetReferenceArray(indexString.c_str(), handle, managedInstance, arrayStorage, i, elementToRemove); + break; + }*/ + case DataType::Texture2D: + { + AssetHandle handle = storage.GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, storage, i, elementToRemove); + break; + } + case DataType::Scene: + { + AssetHandle handle = storage.GetValue(i); + PropertyAssetReferenceArray(indexString.c_str(), handle, storage, i, elementToRemove); + break; + } + } + + ImGui::PopID(); + } + + ImGui::PopID(); + + if (elementToRemove != -1) + { + storage.RemoveAt(elementToRemove); + modified = true; + } + + return modified; + } + +} diff --git a/StarEngine/src/StarEngine/ImGui/UICore.cpp b/StarEngine/src/StarEngine/ImGui/UICore.cpp new file mode 100644 index 00000000..86cb21c9 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/UICore.cpp @@ -0,0 +1,203 @@ +#include "sepch.h" +#include "UICore.h" + +#include "ImGuiUtilities.h" + +#include + +#include + +namespace StarEngine::UI { + + static int s_UIContextID = 0; + static uint32_t s_Counter = 0; + static char s_IDBuffer[16 + 2 + 1] = "##"; + static char s_LabelIDBuffer[1024 + 1]; + + const char* GenerateID() + { + snprintf(s_IDBuffer + 2, 16, "%u", s_Counter++); + return s_IDBuffer; + } + + const char* GenerateLabelID(std::string_view label) + { + *std::format_to_n(s_LabelIDBuffer, std::size(s_LabelIDBuffer), "{}##{}", label, s_Counter++).out = 0; + return s_LabelIDBuffer; + } + + void PushID() + { + ImGui::PushID(s_UIContextID++); + s_Counter = 0; + } + + void PopID() + { + ImGui::PopID(); + s_UIContextID--; + } + + bool IsInputEnabled() + { + const auto& io = ImGui::GetIO(); + return (io.ConfigFlags & ImGuiConfigFlags_NoMouse) == 0 && (io.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard) == 0; + } + + void SetInputEnabled(bool enabled) + { + auto& io = ImGui::GetIO(); + + if (enabled) + { + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; + io.ConfigFlags &= ~ImGuiConfigFlags_NavNoCaptureKeyboard; + } + else + { + io.ConfigFlags |= ImGuiConfigFlags_NoMouse; + io.ConfigFlags |= ImGuiConfigFlags_NavNoCaptureKeyboard; + } + } + + void ShiftCursorX(float distance) + { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + distance); + } + + void ShiftCursorY(float distance) + { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + distance); + } + + void ShiftCursor(float x, float y) + { + const ImVec2 cursor = ImGui::GetCursorPos(); + ImGui::SetCursorPos(ImVec2(cursor.x + x, cursor.y + y)); + } + + void BeginPropertyGrid(uint32_t columns) + { + PushID(); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 8.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 4.0f)); + ImGui::Columns(columns); + } + + void EndPropertyGrid() + { + ImGui::Columns(1); + Draw::Underline(); + ImGui::PopStyleVar(2); // ItemSpacing, FramePadding + ShiftCursorY(18.0f); + PopID(); + } + + bool BeginTreeNode(const char* name, bool defaultOpen) + { + ImGuiTreeNodeFlags treeNodeFlags = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_FramePadding; + if (defaultOpen) + treeNodeFlags |= ImGuiTreeNodeFlags_DefaultOpen; + + return ImGui::TreeNodeEx(name, treeNodeFlags); + } + + void EndTreeNode() + { + ImGui::TreePop(); + } + + bool ColoredButton(const char* label, const ImVec4& backgroundColor, ImVec2 buttonSize) + { + ScopedColour buttonColor(ImGuiCol_Button, backgroundColor); + return ImGui::Button(label, buttonSize); + } + + bool ColoredButton(const char* label, const ImVec4& backgroundColor, const ImVec4& foregroundColor, ImVec2 buttonSize) + { + ScopedColour textColor(ImGuiCol_Text, foregroundColor); + ScopedColour buttonColor(ImGuiCol_Button, backgroundColor); + return ImGui::Button(label, buttonSize); + } + + + bool TableRowClickable(const char* id, float rowHeight) + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + window->DC.CurrLineSize.y = rowHeight; + + ImGui::TableNextRow(0, rowHeight); + ImGui::TableNextColumn(); + + window->DC.CurrLineTextBaseOffset = 3.0f; + const ImVec2 rowAreaMin = ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), 0).Min; + const ImVec2 rowAreaMax = { ImGui::TableGetCellBgRect(ImGui::GetCurrentTable(), ImGui::TableGetColumnCount() - 1).Max.x, rowAreaMin.y + rowHeight }; + + ImGui::PushClipRect(rowAreaMin, rowAreaMax, false); + + bool isRowHovered, held; + bool isRowClicked = ImGui::ButtonBehavior(ImRect(rowAreaMin, rowAreaMax), ImGui::GetID(id), + &isRowHovered, &held, ImGuiButtonFlags_AllowOverlap); + + ImGui::SetItemAllowOverlap(); + ImGui::PopClipRect(); + + return isRowClicked; + } + + void Separator(ImVec2 size, ImVec4 color) + { + ImGui::PushStyleColor(ImGuiCol_ChildBg, color); + ImGui::BeginChild("sep", size); + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + + bool IsWindowFocused(const char* windowName, const bool checkRootWindow) + { + ImGuiWindow* currentNavWindow = GImGui->NavWindow; + + if (checkRootWindow) + { + // Get the actual nav window (not e.g a table) + ImGuiWindow* lastWindow = NULL; + while (lastWindow != currentNavWindow) + { + lastWindow = currentNavWindow; + currentNavWindow = currentNavWindow->RootWindow; + } + } + + return currentNavWindow == ImGui::FindWindowByName(windowName); + } + + void HelpMarker(const char* desc) + { + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } + + bool ImageButton(const Ref& texture, const ImVec2& size, const ImVec4& tint) + { + return ImageButton(texture, size, ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0), tint); + } + + void ImageToolTip(const Ref& texture) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + std::string filepath = texture->GetPath().string(); + ImGui::TextUnformatted(filepath.c_str()); + ImGui::PopTextWrapPos(); + UI::Image(texture, ImVec2(384, 384)); + ImGui::EndTooltip(); + } + +} diff --git a/StarEngine/src/StarEngine/ImGui/UICore.h b/StarEngine/src/StarEngine/ImGui/UICore.h new file mode 100644 index 00000000..8ab2e992 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/UICore.h @@ -0,0 +1,112 @@ +#pragma once + +#include "StarEngine/Renderer/Texture.h" +#include "StarEngine/ImGui/Colors.h" +#include "StarEngine/ImGui/ImGuiUtilities.h" + +#include + +namespace StarEngine::UI { + + const char* GenerateID(); + const char* GenerateLabelID(std::string_view label); + void PushID(); + void PopID(); + + bool IsInputEnabled(); + void SetInputEnabled(bool enabled); + + void ShiftCursorX(float distance); + void ShiftCursorY(float distance); + void ShiftCursor(float x, float y); + + void BeginPropertyGrid(uint32_t columns = 2); + void EndPropertyGrid(); + + bool BeginTreeNode(const char* name, bool defaultOpen = true); + void EndTreeNode(); + + bool ColoredButton(const char* label, const ImVec4& backgroundColor, ImVec2 buttonSize = { 16.0f, 16.0f }); + bool ColoredButton(const char* label, const ImVec4& backgroundColor, const ImVec4& foregroundColor, ImVec2 buttonSize = { 16.0f, 16.0f }); + + + template + static void Table(const char* tableName, const char** columns, uint32_t columnCount, const ImVec2& size, T callback) + { + if (size.x <= 0.0f || size.y <= 0.0f) + return; + + float edgeOffset = 4.0f; + + ScopedStyle cellPadding(ImGuiStyleVar_CellPadding, ImVec2(4.0f, 0.0f)); + ImColor backgroundColor = ImColor(Colors::Theme::background); + const ImColor colRowAlt = ColourWithMultipliedValue(backgroundColor, 1.2f); + ScopedColour rowColor(ImGuiCol_TableRowBg, backgroundColor); + ScopedColour rowAltColor(ImGuiCol_TableRowBgAlt, colRowAlt); + ScopedColour tableColor(ImGuiCol_ChildBg, backgroundColor); + + ImGuiTableFlags flags = ImGuiTableFlags_NoPadInnerX + | ImGuiTableFlags_Resizable + | ImGuiTableFlags_Reorderable + | ImGuiTableFlags_ScrollY + | ImGuiTableFlags_RowBg; + + if (!ImGui::BeginTable(tableName, columnCount, flags, size)) + return; + + const float cursorX = ImGui::GetCursorScreenPos().x; + + for (uint32_t i = 0; i < columnCount; i++) + ImGui::TableSetupColumn(columns[i]); + + // Headers + { + const ImColor activeColor = ColourWithMultipliedValue(backgroundColor, 1.3f); + ScopedColourStack headerCol(ImGuiCol_HeaderHovered, activeColor, ImGuiCol_HeaderActive, activeColor); + + ImGui::TableSetupScrollFreeze(ImGui::TableGetColumnCount(), 1); + ImGui::TableNextRow(ImGuiTableRowFlags_Headers, 22.0f); + + for (uint32_t i = 0; i < columnCount; i++) + { + ImGui::TableSetColumnIndex(i); + const char* columnName = ImGui::TableGetColumnName(i); + ImGui::PushID(columnName); + ShiftCursor(edgeOffset * 3.0f, edgeOffset * 2.0f); + ImGui::TableHeader(columnName); + ShiftCursor(-edgeOffset * 3.0f, -edgeOffset * 2.0f); + ImGui::PopID(); + } + ImGui::SetCursorScreenPos(ImVec2(cursorX, ImGui::GetCursorScreenPos().y)); + Draw::Underline(true, 0.0f, 5.0f); + } + + callback(); + ImGui::EndTable(); + } + + bool TableRowClickable(const char* id, float rowHeight); + void Separator(ImVec2 size, ImVec4 color); + + bool IsWindowFocused(const char* windowName, const bool checkRootWindow = true); + void HelpMarker(const char* desc); + + //========================================================================================= + /// Images / Textures (Requires e.g Vulkan Implementation) + + ImTextureID GetTextureID(Ref image); + ImTextureID GetTextureID(Ref texture); + + void Image(const Ref& image, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + void Image(const Ref& image, uint32_t layer, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + void ImageMip(const Ref& image, uint32_t mip, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + void Image(const Ref& texture, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& tint_col = ImVec4(1, 1, 1, 1), const ImVec4& border_col = ImVec4(0, 0, 0, 0)); + bool ImageButton(const Ref& image, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + bool ImageButton(const char* stringID, const Ref& image, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + bool ImageButton(const Ref& texture, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + bool ImageButton(const char* stringID, const Ref& texture, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + + bool ImageButton(const Ref& texture, const ImVec2& size, const ImVec4& tint); + + void ImageToolTip(const Ref& texture); +} diff --git a/StarEngine/src/StarEngine/ImGui/VulkanImGui.cpp b/StarEngine/src/StarEngine/ImGui/VulkanImGui.cpp new file mode 100644 index 00000000..43da76f5 --- /dev/null +++ b/StarEngine/src/StarEngine/ImGui/VulkanImGui.cpp @@ -0,0 +1,166 @@ +#include "sepch.h" +#include "UICore.h" + +#include "StarEngine/Renderer/RendererAPI.h" +#include "StarEngine/Renderer/Renderer.h" + +#include "../src/vulkan/vulkan-backend.h" + +namespace ImGui { + extern bool ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags); +} + +namespace StarEngine::UI { + + ImTextureID GetTextureID(Ref image) + { + return (ImTextureID)image->GetHandle().Get(); + +#if OLD + if (image && RendererAPI::Current() == RendererAPIType::Vulkan) + { + + nvrhi::vulkan::Texture* vTexture = (nvrhi::vulkan::Texture*)image->GetHandle()->getNativeObject(nvrhi::ObjectTypes::VK_Image).pointer; + const nvrhi::vulkan::TextureSubresourceView& vTextureView = vTexture->getSubresourceView(image->GetImageInfo().ImageView, nvrhi::TextureDimension::Texture2D, + Utils::NVRHIFormat(image->GetSpecification().Format), vk::ImageUsageFlagBits::eSampled); + + nvrhi::vulkan::Sampler* sampler = nullptr; + if (image->GetImageInfo().Sampler) + sampler = (nvrhi::vulkan::Sampler*)image->GetImageInfo().Sampler->getNativeObject(nvrhi::ObjectTypes::VK_Sampler).pointer; + else + sampler = (nvrhi::vulkan::Sampler*)Renderer::GetClampSampler()->getNativeObject(nvrhi::ObjectTypes::VK_Sampler).pointer; + if (vTextureView.view) + //return ImGui_ImplVulkan_AddTexture(sampler->sampler, vTextureView.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); // TODO(Yan): image layout is hax + return ImGui_ImplVulkan_AddTexture(vTexture); + } + + return (ImTextureID)0; +#endif + } + + ImTextureID GetTextureIDLayer(Ref image, uint32_t imageLayer) + { + return (ImTextureID)image->GetHandle().Get(); +#if OLD + if (image && RendererAPI::Current() == RendererAPIType::Vulkan) + { + nvrhi::vulkan::Texture* vTexture = (nvrhi::vulkan::Texture*)image->GetHandle()->getNativeObject(nvrhi::ObjectTypes::VK_Image).pointer; + const nvrhi::vulkan::TextureSubresourceView& vTextureView = vTexture->getSubresourceView(image->GetLayerImageView(imageLayer), nvrhi::TextureDimension::Texture2D, + Utils::NVRHIFormat(image->GetSpecification().Format), vk::ImageUsageFlagBits::eSampled); + nvrhi::vulkan::Sampler* sampler = nullptr; + if (image->GetImageInfo().Sampler) + sampler = (nvrhi::vulkan::Sampler*)image->GetImageInfo().Sampler->getNativeObject(nvrhi::ObjectTypes::VK_Sampler).pointer; + else + sampler = (nvrhi::vulkan::Sampler*)Renderer::GetClampSampler()->getNativeObject(nvrhi::ObjectTypes::VK_Sampler).pointer; + if (vTextureView.view) + return ImGui_ImplVulkan_AddTexture(vTexture); + } + + return (ImTextureID)0; +#endif + } + + ImTextureID GetTextureIDMip(Ref image, uint32_t mip) + { + return (ImTextureID)image->GetHandle().Get(); +#if OLD + if (image && RendererAPI::Current() == RendererAPIType::Vulkan) + { + nvrhi::vulkan::Texture* vTexture = (nvrhi::vulkan::Texture*)image->GetHandle()->getNativeObject(nvrhi::ObjectTypes::VK_Image).pointer; + const nvrhi::vulkan::TextureSubresourceView& vTextureView = vTexture->getSubresourceView(image->GetMipImageView(mip), nvrhi::TextureDimension::Texture2D, + Utils::NVRHIFormat(image->GetSpecification().Format), vk::ImageUsageFlagBits::eSampled); + nvrhi::vulkan::Sampler* sampler = nullptr; + if (image->GetImageInfo().Sampler) + sampler = (nvrhi::vulkan::Sampler*)image->GetImageInfo().Sampler->getNativeObject(nvrhi::ObjectTypes::VK_Sampler).pointer; + else + sampler = (nvrhi::vulkan::Sampler*)Renderer::GetClampSampler()->getNativeObject(nvrhi::ObjectTypes::VK_Sampler).pointer; + if (vTextureView.view) + return ImGui_ImplVulkan_AddTexture(vTexture); // TODO(Yan): image layout is hax + } + + return (ImTextureID)0; +#endif + } + + ImTextureID GetTextureID(Ref texture) + { + return GetTextureID(texture->GetImage()); + } + + void Image(const Ref& image, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) + { + SE_CORE_VERIFY(image, "Image is null"); + + const auto textureID = GetTextureID(image); + ImGui::Image(textureID, size, uv0, uv1, tint_col, border_col); + } + + void Image(const Ref& image, uint32_t imageLayer, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) + { + SE_CORE_VERIFY(image, "Image is null"); + + const auto textureID = GetTextureIDLayer(image, imageLayer); + ImGui::Image(textureID, size, uv0, uv1, tint_col, border_col); + } + + void ImageMip(const Ref& image, uint32_t mip, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) + { + SE_CORE_VERIFY(image, "Image is null"); + + const auto textureID = GetTextureIDMip(image, mip); + ImGui::Image(textureID, size, uv0, uv1, tint_col, border_col); + } + + void Image(const Ref& texture, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) + { + SE_CORE_VERIFY(texture, "Texture is null"); + + const auto textureID = GetTextureID(texture->GetImage()); + ImGui::Image(textureID, size, uv0, uv1, tint_col, border_col); + } + + bool ImageButton(const Ref& image, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) + { + return ImageButton(nullptr, image, size, uv0, uv1, bg_col, tint_col); + } + + bool ImageButton(const char* stringID, const Ref& image, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) + { + const auto textureID = GetTextureID(image); + ImGuiID id = (ImGuiID)((((uint64_t)textureID) >> 32) ^ (uint32_t)(uint64_t)textureID); + if (stringID) + { + const ImGuiID strID = ImGui::GetID(stringID); + id = id ^ strID; + } + return ImGui::ImageButtonEx(id, textureID, size, uv0, uv1, bg_col, tint_col); + + SE_CORE_VERIFY(false, "Not supported"); + return false; + } + + bool ImageButton(const Ref& texture, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) + { + return ImageButton(nullptr, texture, size, uv0, uv1, bg_col, tint_col); + } + + bool ImageButton(const char* stringID, const Ref& texture, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) + { + SE_CORE_VERIFY(texture); + if (!texture) + return false; + + const auto textureID = GetTextureID(texture); + ImGuiID id = (ImGuiID)((((uint64_t)textureID) >> 32) ^ (uint32_t)(uint64_t)textureID); + if (stringID) + { + const ImGuiID strID = ImGui::GetID(stringID); + id = id ^ strID; + } + return ImGui::ImageButtonEx(id, textureID, size, uv0, uv1, bg_col, tint_col); + + SE_CORE_VERIFY(false, "Not supported"); + return false; + } + +} diff --git a/StarEngine/src/StarEngine/Math/Math.cpp b/StarEngine/src/StarEngine/Math/Math.cpp index 032815c1..aa46f4f8 100644 --- a/StarEngine/src/StarEngine/Math/Math.cpp +++ b/StarEngine/src/StarEngine/Math/Math.cpp @@ -1,40 +1,92 @@ #include "sepch.h" -#include "StarEngine/Math/Math.h" +#include "Math.h" #define GLM_ENABLE_EXPERIMENTAL -#include +#include +#include -namespace StarEngine::Math -{ - bool DecomposeTransform(const glm::mat4& transform, glm::vec3& translation, glm::vec3& rotation, glm::vec3& scale) +namespace StarEngine::Math { + + glm::vec3 Scale(const glm::vec3& v, float desiredLength) { - // From glm::decompose in matrix_decompose.inl + float mag = glm::length(v); + if (glm::epsilonEqual(mag, 0.0f, glm::epsilon())) + return glm::vec3(0.0f); + + return v * desiredLength / mag; + } + bool DecomposeTransform(const glm::mat4& transform, glm::vec3& translation, glm::quat& rotation, glm::vec3& scale) + { using namespace glm; using T = float; mat4 LocalMatrix(transform); - // Normalize the matrix. - if (epsilonEqual(LocalMatrix[3][3], static_cast(0), epsilon())) + if (epsilonEqual(LocalMatrix[3][3], static_cast(0), epsilon())) return false; - // First, isolate perspective. This is the messiest. - if ( - epsilonNotEqual(LocalMatrix[0][3], static_cast(0), epsilon()) || - epsilonNotEqual(LocalMatrix[1][3], static_cast(0), epsilon()) || - epsilonNotEqual(LocalMatrix[2][3], static_cast(0), epsilon())) - { - // Clear the perspective partition - LocalMatrix[0][3] = LocalMatrix[1][3] = LocalMatrix[2][3] = static_cast(0); - LocalMatrix[3][3] = static_cast(1); - } + // Assume matrix is already normalized + SE_CORE_ASSERT(epsilonEqual(LocalMatrix[3][3], static_cast(1), static_cast(0.00001))); + //for (length_t i = 0; i < 4; ++i) + // for (length_t j = 0; j < 4; ++j) + // LocalMatrix[i][j] /= LocalMatrix[3][3]; + + // Ignore perspective + SE_CORE_ASSERT( + epsilonEqual(LocalMatrix[0][3], static_cast(0), epsilon()) && + epsilonEqual(LocalMatrix[1][3], static_cast(0), epsilon()) && + epsilonEqual(LocalMatrix[2][3], static_cast(0), epsilon()) + ); + //// perspectiveMatrix is used to solve for perspective, but it also provides + //// an easy way to test for singularity of the upper 3x3 component. + //mat<4, 4, T, Q> PerspectiveMatrix(LocalMatrix); + // + //for (length_t i = 0; i < 3; i++) + // PerspectiveMatrix[i][3] = static_cast(0); + //PerspectiveMatrix[3][3] = static_cast(1); + // + ///// TODO: Fixme! + //if (epsilonEqual(determinant(PerspectiveMatrix), static_cast(0), epsilon())) + // return false; + // + //// First, isolate perspective. This is the messiest. + //if ( + // epsilonNotEqual(LocalMatrix[0][3], static_cast(0), epsilon()) || + // epsilonNotEqual(LocalMatrix[1][3], static_cast(0), epsilon()) || + // epsilonNotEqual(LocalMatrix[2][3], static_cast(0), epsilon())) + //{ + // // rightHandSide is the right hand side of the equation. + // vec<4, T, Q> RightHandSide; + // RightHandSide[0] = LocalMatrix[0][3]; + // RightHandSide[1] = LocalMatrix[1][3]; + // RightHandSide[2] = LocalMatrix[2][3]; + // RightHandSide[3] = LocalMatrix[3][3]; + // + // // Solve the equation by inverting PerspectiveMatrix and multiplying + // // rightHandSide by the inverse. (This is the easiest way, not + // // necessarily the best.) + // mat<4, 4, T, Q> InversePerspectiveMatrix = glm::inverse(PerspectiveMatrix);// inverse(PerspectiveMatrix, inversePerspectiveMatrix); + // mat<4, 4, T, Q> TransposedInversePerspectiveMatrix = glm::transpose(InversePerspectiveMatrix);// transposeMatrix4(inversePerspectiveMatrix, transposedInversePerspectiveMatrix); + // + // Perspective = TransposedInversePerspectiveMatrix * RightHandSide; + // // v4MulPointByMatrix(rightHandSide, transposedInversePerspectiveMatrix, perspectivePoint); + // + // // Clear the perspective partition + // LocalMatrix[0][3] = LocalMatrix[1][3] = LocalMatrix[2][3] = static_cast(0); + // LocalMatrix[3][3] = static_cast(1); + //} + //else + //{ + // // No perspective. + // Perspective = vec<4, T, Q>(0, 0, 0, 1); + //} // Next take care of translation (easy). translation = vec3(LocalMatrix[3]); LocalMatrix[3] = vec4(0, 0, 0, LocalMatrix[3].w); - vec3 Row[3], Pdum3; + vec3 Row[3]; // Now get scale and shear. for (length_t i = 0; i < 3; ++i) @@ -43,39 +95,96 @@ namespace StarEngine::Math // Compute X scale factor and normalize first row. scale.x = length(Row[0]); - Row[0] = detail::scale(Row[0], static_cast(1)); + Row[0] = Scale(Row[0], static_cast(1)); + + // Ignore shear + //// Compute XY shear factor and make 2nd row orthogonal to 1st. + //Skew.z = dot(Row[0], Row[1]); + //Row[1] = detail::combine(Row[1], Row[0], static_cast(1), -Skew.z); + + // Now, compute Y scale and normalize 2nd row. scale.y = length(Row[1]); - Row[1] = detail::scale(Row[1], static_cast(1)); + Row[1] = Scale(Row[1], static_cast(1)); + //Skew.z /= Scale.y; + + //// Compute XZ and YZ shears, orthogonalize 3rd row. + //Skew.y = glm::dot(Row[0], Row[2]); + //Row[2] = detail::combine(Row[2], Row[0], static_cast(1), -Skew.y); + //Skew.x = glm::dot(Row[1], Row[2]); + //Row[2] = detail::combine(Row[2], Row[1], static_cast(1), -Skew.x); + + // Next, get Z scale and normalize 3rd row. scale.z = length(Row[2]); - Row[2] = detail::scale(Row[2], static_cast(1)); + Row[2] = Scale(Row[2], static_cast(1)); + //Skew.y /= Scale.z; + //Skew.x /= Scale.z; +#if _DEBUG // At this point, the matrix (in rows[]) is orthonormal. // Check for a coordinate system flip. If the determinant // is -1, then negate the matrix and the scaling factors. -#if 0 - Pdum3 = cross(Row[1], Row[2]); // v3Cross(row[1], row[2], Pdum3); - if (dot(Row[0], Pdum3) < 0) - { - for (length_t i = 0; i < 3; i++) - { - scale[i] *= static_cast(-1); - Row[i] *= static_cast(-1); - } - } + vec3 Pdum3 = cross(Row[1], Row[2]); // v3Cross(row[1], row[2], Pdum3); + SE_CORE_ASSERT(dot(Row[0], Pdum3) >= static_cast(0)); #endif + //if (dot(Row[0], Pdum3) < 0) + //{ + // for (length_t i = 0; i < 3; i++) + // { + // scale[i] *= static_cast(-1); + // Row[i] *= static_cast(-1); + // } + //} + + // Rotation as XYZ Euler angles + //rotation.y = asin(-Row[0][2]); + //if (cos(rotation.y) != 0.f) + //{ + // rotation.x = atan2(Row[1][2], Row[2][2]); + // rotation.z = atan2(Row[0][1], Row[0][0]); + //} + //else + //{ + // rotation.x = atan2(-Row[2][0], Row[1][1]); + // rotation.z = 0; + //} + + // Rotation as quaternion + int i, j, k = 0; + T root, trace = Row[0].x + Row[1].y + Row[2].z; + if (trace > static_cast(0)) + { + root = sqrt(trace + static_cast(1)); + rotation.w = static_cast(0.5) * root; + root = static_cast(0.5) / root; + rotation.x = root * (Row[1].z - Row[2].y); + rotation.y = root * (Row[2].x - Row[0].z); + rotation.z = root * (Row[0].y - Row[1].x); + } // End if > 0 + else + { + static int Next[3] = { 1, 2, 0 }; + i = 0; + if (Row[1].y > Row[0].x) i = 1; + if (Row[2].z > Row[i][i]) i = 2; + j = Next[i]; + k = Next[j]; - rotation.y = asin(-Row[0][2]); - if (cos(rotation.y) != 0) { - rotation.x = atan2(Row[1][2], Row[2][2]); - rotation.z = atan2(Row[0][1], Row[0][0]); - } - else { - rotation.x = atan2(-Row[2][0], Row[1][1]); - rotation.z = 0; - } + root = sqrt(Row[i][i] - Row[j][j] - Row[k][k] + static_cast(1.0)); + rotation[i] = static_cast(0.5) * root; + root = static_cast(0.5) / root; + rotation[j] = root * (Row[i][j] + Row[j][i]); + rotation[k] = root * (Row[i][k] + Row[k][i]); + rotation.w = root * (Row[j][k] - Row[k][j]); + } // End if <= 0 return true; } + + glm::mat4 ComposeTransform(const glm::vec3& translation, const glm::quat& rotation, const glm::vec3& scale) + { + return glm::translate(glm::mat4(1.0f), translation) * glm::mat4_cast(rotation) * glm::scale(glm::mat4(1.0f), scale); + } + } diff --git a/StarEngine/src/StarEngine/Math/Math.h b/StarEngine/src/StarEngine/Math/Math.h index 5f3b9c49..c9cfcfb0 100644 --- a/StarEngine/src/StarEngine/Math/Math.h +++ b/StarEngine/src/StarEngine/Math/Math.h @@ -1,7 +1,24 @@ #pragma once -#include - namespace StarEngine::Math { - bool DecomposeTransform(const glm::mat4& transform, glm::vec3& translation, glm::vec3& rotation, glm::vec3& scale); + + // Decompose a transform matrix into translation, rotation, and scale components. + bool DecomposeTransform(const glm::mat4& transform, glm::vec3& translation, glm::quat& rotation, glm::vec3& scale); + + // Compose a transform matrix from translation, rotation, and scale components. + glm::mat4 ComposeTransform(const glm::vec3& translation, const glm::quat& rotation, const glm::vec3& scale); + + //TODO: Replace with a C++20 concept? + template, bool> = true> + inline static T DivideAndRoundUp(T dividend, T divisor) + { + return (dividend + divisor - 1) / divisor; + } + + // Only integers are allowed + template&& std::is_integral_v, bool> = true> + inline static T DivideAndRoundUp(T dividend, DivisorT divisor) + { + return { DivideAndRoundUp(dividend.x, divisor), DivideAndRoundUp(dividend.y, divisor) }; + } } diff --git a/StarEngine/src/StarEngine/Physics/CharacterController.h b/StarEngine/src/StarEngine/Physics/CharacterController.h new file mode 100644 index 00000000..085f411b --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/CharacterController.h @@ -0,0 +1,54 @@ +#pragma once + +#include "StarEngine/Core/Ref.h" +#include "StarEngine/Scene/Entity.h" + +#include "PhysicsTypes.h" + +#include + +namespace StarEngine { + + class CharacterController : public RefCounted + { + public: + virtual ~CharacterController() = default; + + virtual void SetGravityEnabled(bool enableGravity) = 0; + virtual bool IsGravityEnabled() const = 0; + + virtual void SetSlopeLimit(float slopeLimit) = 0; + virtual void SetStepOffset(float stepOffset) = 0; + + // instantly set the translation/rotation of the character. aka "teleport" + virtual void SetTranslation(const glm::vec3& inTranslation) = 0; + virtual void SetRotation(const glm::quat& inRotation) = 0; + + virtual bool IsGrounded() const = 0; + + virtual void SetControlMovementInAir(bool controlMovementInAir) = 0; + virtual bool CanControlMovementInAir() const = 0; + virtual void SetControlRotationInAir(bool controlRotationInAir) = 0; + virtual bool CanControlRotationInAir() const = 0; + + virtual ECollisionFlags GetCollisionFlags() const = 0; + + // Incrementally move/rotate/jump the character during physics simulation + // Unless you know you want to teleport the character, prefer these functions over SetTranslation/SetRotation + virtual void Move(const glm::vec3& displacement) = 0; + virtual void Rotate(const glm::quat& rotation) = 0; + virtual void Jump(float jumpPower) = 0; + + virtual glm::vec3 GetLinearVelocity() const = 0; + virtual void SetLinearVelocity(const glm::vec3& inVelocity) = 0; + + private: + virtual void PreSimulate(float deltaTime) {} + virtual void Simulate(float deltaTime) = 0; + virtual void PostSimulate() {} + + private: + friend class PhysicsScene; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/ColliderMaterial.h b/StarEngine/src/StarEngine/Physics/ColliderMaterial.h new file mode 100644 index 00000000..d0fa04fb --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/ColliderMaterial.h @@ -0,0 +1,11 @@ +#pragma once + +namespace StarEngine { + + struct ColliderMaterial + { + float Friction = 0.5f; + float Restitution = 0.15f; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/ContactListener2D.cpp b/StarEngine/src/StarEngine/Physics/ContactListener2D.cpp deleted file mode 100644 index eda54680..00000000 --- a/StarEngine/src/StarEngine/Physics/ContactListener2D.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "sepch.h" -#include "ContactListener2D.h" - -#include "StarEngine/Scene/Entity.h" -#include "StarEngine/Scripting/ScriptEngine.h" -#include "StarEngine/Scene/ScriptableEntity.h" - -namespace StarEngine { - - void ContactListener2D::BeginContact(b2Contact* contact) - { - if (m_IsPlaying) - { - Entity& a = *(Entity*)contact->GetFixtureA()->GetBody()->GetUserData().pointer; - Entity& b = *(Entity*)contact->GetFixtureB()->GetBody()->GetUserData().pointer; - - if (a.HasComponent() && a.HasComponent()) - { - auto& scriptComponent = a.GetComponent(); - if (scriptComponent.Instance) // Ensure the shared_ptr is not null - { - scriptComponent.Instance->Invoke("OnCollisionBegin"); - } - } - - if (b.HasComponent() && b.HasComponent()) - { - auto& scriptComponent = b.GetComponent(); - if (scriptComponent.Instance) // Ensure the shared_ptr is not null - { - scriptComponent.Instance->Invoke("OnCollisionBegin"); - } - } - - } - } - - /// Called when two fixtures cease to touch. - void ContactListener2D::EndContact(b2Contact* contact) - { - if (m_IsPlaying) - { - Entity& a = *(Entity*)contact->GetFixtureA()->GetBody()->GetUserData().pointer; - Entity& b = *(Entity*)contact->GetFixtureB()->GetBody()->GetUserData().pointer; - - if (a.HasComponent() && a.HasComponent()) - { - auto& scriptComponent = a.GetComponent(); - if (scriptComponent.Instance) // Ensure the shared_ptr is not null - { - scriptComponent.Instance->Invoke("OnCollisionEnd"); - } - } - - if (b.HasComponent() && b.HasComponent()) - { - auto& scriptComponent = b.GetComponent(); - if (scriptComponent.Instance) // Ensure the shared_ptr is not null - { - scriptComponent.Instance->Invoke("OnCollisionEnd"); - } - } - } - } - -} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltAPI.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltAPI.cpp new file mode 100644 index 00000000..9565be55 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltAPI.cpp @@ -0,0 +1,126 @@ +#include "sepch.h" +#include "JoltAPI.h" + +#include "JoltScene.h" +#include "JoltCookingFactory.h" +#include "JoltCaptureManager.h" + +#include +#include +#include +#include +#include +#include + +namespace StarEngine { + + struct JoltData + { + JPH::TempAllocator* TemporariesAllocator; + std::unique_ptr JobThreadPool; + + std::string LastErrorMessage = ""; + + Ref CookingFactory = nullptr; + Ref CaptureManager = nullptr; + }; + + static JoltData* s_JoltData = nullptr; + + static void JoltTraceCallback(const char* format, ...) + { + va_list list; + va_start(list, format); + char buffer[1024]; + vsnprintf(buffer, sizeof(buffer), format, list); + + if (s_JoltData) + { + s_JoltData->LastErrorMessage = buffer; + } + SE_CORE_TRACE_TAG("Physics", buffer); + } + +#ifdef JPH_ENABLE_ASSERTS + + static bool JoltAssertFailedCallback(const char* expression, const char* message, const char* file, uint32_t line) + { + SE_CORE_FATAL_TAG("Physics", "{}:{}: ({}) {}", file, line, expression, message != nullptr ? message : ""); + return true; + } + +#endif + + JoltAPI::JoltAPI() + { + + } + + JoltAPI::~JoltAPI() + { + + } + +#define SE_ENABLE_MALLOC_ALLOC 0 + + void JoltAPI::Init() + { + SE_CORE_VERIFY(!s_JoltData, "Can't initialize Jolt multiple times!"); + + JPH::RegisterDefaultAllocator(); + + JPH::Trace = JoltTraceCallback; + +#ifdef JPH_ENABLE_ASSERTS + JPH::AssertFailed = JoltAssertFailedCallback; +#endif + + JPH::Factory::sInstance = new JPH::Factory(); + + JPH::RegisterTypes(); + + s_JoltData = snew JoltData(); + +#if SE_ENABLE_MALLOC_ALLOC + // NOTE(Peter): We shouldn't be using this allocator if we want the best performance + s_JoltData->TemporariesAllocator = new JPH::TempAllocatorMalloc(); +#else + s_JoltData->TemporariesAllocator = new JPH::TempAllocatorImpl(300 * 1024 * 1024); // 10 mb +#endif + + // NOTE(Peter): Just construct the thread pool for now, don't start the threads + s_JoltData->JobThreadPool = std::make_unique(2048, 8, 6); + + s_JoltData->CookingFactory = Ref::Create(); + s_JoltData->CookingFactory->Init(); + + s_JoltData->CaptureManager = Ref::Create(); + } + + void JoltAPI::Shutdown() + { + s_JoltData->CaptureManager = nullptr; + + s_JoltData->CookingFactory->Shutdown(); + s_JoltData->CookingFactory = nullptr; + + delete s_JoltData->TemporariesAllocator; + + sdelete s_JoltData; + s_JoltData = nullptr; + + delete JPH::Factory::sInstance; + } + + JPH::TempAllocator* JoltAPI::GetTempAllocator() const { return s_JoltData->TemporariesAllocator; } + JPH::JobSystemThreadPool* JoltAPI::GetJobThreadPool() const { return s_JoltData->JobThreadPool.get(); } + + const std::string& JoltAPI::GetLastErrorMessage() const { return s_JoltData->LastErrorMessage; } + + Ref JoltAPI::CreateScene(const Ref& scene) const { return Ref::Create(scene); } + + Ref JoltAPI::GetMeshCookingFactory() const { return s_JoltData->CookingFactory; } + + Ref JoltAPI::GetCaptureManager() const { return s_JoltData->CaptureManager; } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltAPI.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltAPI.h new file mode 100644 index 00000000..0ddc47ae --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltAPI.h @@ -0,0 +1,35 @@ +#pragma once + +#include "StarEngine/Physics/PhysicsAPI.h" + +#ifdef SE_ENABLE_ASSERTS +#define JPH_ENABLE_ASSERTS +#endif + +#include +#include +#include +#include + +namespace StarEngine { + + class JoltAPI : public PhysicsAPI + { + public: + JoltAPI(); + ~JoltAPI(); + + virtual void Init() override; + virtual void Shutdown() override; + + JPH::TempAllocator* GetTempAllocator() const; + JPH::JobSystemThreadPool* GetJobThreadPool() const; + + virtual const std::string& GetLastErrorMessage() const override; + virtual Ref CreateScene(const Ref& scene) const override; + + virtual Ref GetMeshCookingFactory() const override; + virtual Ref GetCaptureManager() const override; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBinaryStream.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBinaryStream.cpp new file mode 100644 index 00000000..d020d5ee --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBinaryStream.cpp @@ -0,0 +1,3 @@ +#include "sepch.h" +#include "JoltBinaryStream.h" + diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBinaryStream.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBinaryStream.h new file mode 100644 index 00000000..b7f62609 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBinaryStream.h @@ -0,0 +1,63 @@ +#pragma once + +#include "StarEngine/Core/Buffer.h" + +#include +#include + +namespace StarEngine { + + class JoltBinaryStreamReader : public JPH::StreamIn + { + public: + JoltBinaryStreamReader(const Buffer& buffer) + : m_Buffer(&buffer) + { + } + + ~JoltBinaryStreamReader() + { + m_Buffer = nullptr; + m_ReadBytes = 0; + } + + virtual void ReadBytes(void* outData, size_t inNumBytes) override + { + memcpy(outData, ((byte*)m_Buffer->Data) + m_ReadBytes, inNumBytes); + m_ReadBytes += inNumBytes; + } + + virtual bool IsEOF() const override { return m_Buffer == nullptr || m_ReadBytes > m_Buffer->Size; } + + virtual bool IsFailed() const override + { + return m_Buffer == nullptr || m_Buffer->Data == nullptr || m_Buffer->Size == 0; + } + + private: + const Buffer* m_Buffer = nullptr; + size_t m_ReadBytes = 0; + }; + + class JoltBinaryStreamWriter : public JPH::StreamOut + { + public: + void WriteBytes(const void* inData, size_t inNumBytes) override + { + size_t currentOffset = m_TempBuffer.size(); + m_TempBuffer.resize(currentOffset + inNumBytes); + memcpy(m_TempBuffer.data() + currentOffset, inData, inNumBytes); + } + + bool IsFailed() const override { return false; } + + Buffer ToBuffer() const + { + return Buffer::Copy(m_TempBuffer.data(), (uint32_t)m_TempBuffer.size()); + } + + private: + std::vector m_TempBuffer; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBody.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBody.cpp new file mode 100644 index 00000000..83290e6f --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBody.cpp @@ -0,0 +1,741 @@ +#include "sepch.h" +#include "JoltBody.h" + +#include "JoltUtils.h" +#include "JoltShapes.h" +#include "JoltScene.h" +#include "JoltLayerInterface.h" + +#include "StarEngine/Physics/PhysicsSystem.h" + +#include +#include +#include +#include + +#include +using namespace magic_enum::bitwise_operators; + +namespace StarEngine { + + JoltBody::JoltBody(JPH::BodyInterface& bodyInterface, Entity entity) + : PhysicsBody(entity) + { + const auto& rigidBodyComponent = entity.GetComponent(); + + switch (rigidBodyComponent.BodyType) + { + case EBodyType::Static: + { + CreateStaticBody(bodyInterface); + break; + } + case EBodyType::Dynamic: + case EBodyType::Kinematic: + { + CreateDynamicBody(bodyInterface); + break; + } + } + + m_OldMotionType = JoltUtils::ToJoltMotionType(rigidBodyComponent.BodyType); + } + + JoltBody::~JoltBody() + { + + } + + void JoltBody::SetCollisionLayer(uint32_t layerID) + { + JoltScene::GetBodyInterface().SetObjectLayer(m_BodyID, JPH::ObjectLayer(layerID)); + } + + bool JoltBody::IsStatic() const { return JoltScene::GetBodyInterface().GetMotionType(m_BodyID) == JPH::EMotionType::Static; } + bool JoltBody::IsDynamic() const { return JoltScene::GetBodyInterface().GetMotionType(m_BodyID) == JPH::EMotionType::Dynamic; } + bool JoltBody::IsKinematic() const { return JoltScene::GetBodyInterface().GetMotionType(m_BodyID) == JPH::EMotionType::Kinematic; } + + + void JoltBody::MoveKinematic(const glm::vec3& targetPosition, const glm::quat& targetRotation, float deltaSeconds) + { + JPH::BodyLockWrite bodyLock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(bodyLock.Succeeded()); + + JPH::Body& body = bodyLock.GetBody(); + + if (body.GetMotionType() != JPH::EMotionType::Kinematic) + { + SE_CONSOLE_LOG_ERROR("Cannot call MoveKinematic() on a non-kinematic body!"); + return; + } + + body.MoveKinematic(JoltUtils::ToJoltVector(targetPosition), JoltUtils::ToJoltQuat(targetRotation), deltaSeconds); + } + + + void JoltBody::Rotate(const glm::vec3& inRotationTimesDeltaTime) + { + JPH::BodyLockWrite bodyLock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(bodyLock.Succeeded()); + + JPH::Body& body = bodyLock.GetBody(); + + if (body.GetMotionType() != JPH::EMotionType::Kinematic) + { + SE_CONSOLE_LOG_ERROR("Cannot call Rotate() on a non-kinematic body!"); + return; + } + + body.AddRotationStep(JoltUtils::ToJoltVector(inRotationTimesDeltaTime)); + } + + + void JoltBody::SetGravityEnabled(bool isEnabled) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set gravity on a non-dynamic body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + lock.GetBody().GetMotionProperties()->SetGravityFactor(isEnabled ? 1.0f : 0.0f); + } + + + void JoltBody::AddForce(const glm::vec3& force, EForceMode forceMode, bool forceWake /*= true*/) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot apply force to a non-dynamic body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + auto& body = lock.GetBody(); + + switch (forceMode) + { + case EForceMode::Force: + body.AddForce(JoltUtils::ToJoltVector(force)); + break; + case EForceMode::Impulse: + body.AddImpulse(JoltUtils::ToJoltVector(force)); + break; + case EForceMode::VelocityChange: + { + body.SetLinearVelocityClamped(body.GetLinearVelocity() + JoltUtils::ToJoltVector(force)); + + // Don't try to activate the body if the velocity is near zero + if (body.GetLinearVelocity().IsNearZero()) + return; + + break; + } + case EForceMode::Acceleration: + { + // Acceleration can be applied by adding it as a force divided by the inverse mass, since that negates + // the mass inclusion in the integration steps (which is what differentiates a force being applied vs. an acceleration) + body.AddForce(JoltUtils::ToJoltVector(force) / body.GetMotionProperties()->GetInverseMass()); + break; + } + } + + // Use the non-locking version of the body interface. We've already grabbed a lock for the body + if (body.IsInBroadPhase() && !body.IsActive()) + JoltScene::GetBodyInterface(false).ActivateBody(m_BodyID); + } + + + void JoltBody::AddForce(const glm::vec3& force, const glm::vec3& location, EForceMode forceMode, bool forceWake /*= true*/) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot apply force to a non-dynamic body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + auto& body = lock.GetBody(); + + switch (forceMode) + { + case EForceMode::Force: + body.AddForce(JoltUtils::ToJoltVector(force), JoltUtils::ToJoltVector(location)); + break; + case EForceMode::Impulse: + body.AddImpulse(JoltUtils::ToJoltVector(force), JoltUtils::ToJoltVector(location)); + break; + case EForceMode::VelocityChange: + { + // NOTE(Peter): Can't change the velocity at a specific point as far as I can tell... + body.SetLinearVelocityClamped(body.GetLinearVelocity() + JoltUtils::ToJoltVector(force)); + + // Don't try to activate the body if the velocity is near zero + if (body.GetLinearVelocity().IsNearZero()) + return; + + break; + } + case EForceMode::Acceleration: + { + // Acceleration can be applied by adding it as a force divided by the inverse mass, since that negates + // the mass inclusion in the integration steps (which is what differentiates a force being applied vs. an acceleration) + body.AddForce(JoltUtils::ToJoltVector(force) / body.GetMotionProperties()->GetInverseMass(), JoltUtils::ToJoltVector(location)); + break; + } + } + + // Use the non-locking version of the body interface. We've already grabbed a lock for the body + if (body.IsInBroadPhase() && !body.IsActive()) + JoltScene::GetBodyInterface(false).ActivateBody(m_BodyID); + } + + + void JoltBody::AddTorque(const glm::vec3& torque, bool forceWake /*= true*/) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot apply torque to a non-dynamic body!"); + return; + } + + JoltScene::GetBodyInterface().AddTorque(m_BodyID, JoltUtils::ToJoltVector(torque)); + } + + + void JoltBody::AddRadialImpulse(const glm::vec3& origin, float radius, float strength, EFalloffMode falloff, bool velocityChange) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot apply radial impulse to a non-dynamic body!"); + return; + } + + JPH::BodyLockRead readLock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(readLock.Succeeded()); + + const JPH::Body& bodyRead = readLock.GetBody(); + + glm::vec3 centerOfMassPosition = JoltUtils::FromJoltVector(bodyRead.GetCenterOfMassPosition()); + + readLock.ReleaseLock(); + + glm::vec3 direction = centerOfMassPosition - origin; + + float distance = glm::length(direction); + + if (distance > radius) + return; + + direction = glm::normalize(direction); + + float impulseMagnitude = strength; + + if (falloff == EFalloffMode::Linear) + impulseMagnitude *= (1.0f - (distance / radius)); + + glm::vec3 impulse = direction * impulseMagnitude; + + JPH::BodyLockWrite writeLock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(writeLock.Succeeded()); + JPH::Body& bodyWrite = writeLock.GetBody(); + + if (velocityChange) + { + JPH::Vec3 linearVelocity = bodyWrite.GetLinearVelocity() + JoltUtils::ToJoltVector(impulse); + bodyWrite.SetLinearVelocityClamped(linearVelocity); + + if (linearVelocity.IsNearZero()) + return; // Return so that we don't active the body + } + else + { + bodyWrite.AddImpulse(JoltUtils::ToJoltVector(impulse)); + } + + if (!bodyWrite.IsActive()) + JoltScene::GetBodyInterface(false).ActivateBody(m_BodyID); + } + + + void JoltBody::ChangeTriggerState(bool isTrigger) + { + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + lock.GetBody().SetIsSensor(isTrigger); + } + + bool JoltBody::IsTrigger() const + { + JPH::BodyLockRead lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + return lock.GetBody().IsSensor(); + } + + float JoltBody::GetMass() const + { + if (IsStatic()) + { + SE_CONSOLE_LOG_ERROR("Static body does not have mass!"); + return 0.0f; + } + + JPH::BodyLockRead lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + const JPH::Body& body = lock.GetBody(); + return 1.0f / body.GetMotionProperties()->GetInverseMass(); + } + + void JoltBody::SetMass(float mass) + { + if (IsStatic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set mass on a static body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + + JPH::Body& body = lock.GetBody(); + JPH::MassProperties massProperties = body.GetShape()->GetMassProperties(); + massProperties.ScaleToMass(mass); + massProperties.mInertia(3, 3) = 1.0f; + body.GetMotionProperties()->SetMassProperties(JPH::EAllowedDOFs::All, massProperties); + } + + void JoltBody::SetLinearDrag(float inLinearDrag) + { + if (IsStatic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set linear drag on a static body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + JPH::Body& body = lock.GetBody(); + body.GetMotionProperties()->SetLinearDamping(inLinearDrag); + } + + void JoltBody::SetAngularDrag(float inAngularDrag) + { + if (IsStatic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set angular drag on a static body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + JPH::Body& body = lock.GetBody(); + body.GetMotionProperties()->SetAngularDamping(inAngularDrag); + } + + glm::vec3 JoltBody::GetLinearVelocity() const + { + if (IsStatic()) + { + return glm::vec3(0.0f); + } + + return JoltUtils::FromJoltVector(JoltScene::GetBodyInterface().GetLinearVelocity(m_BodyID)); + } + + void JoltBody::SetLinearVelocity(const glm::vec3& inVelocity) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set linear velocity on a non-dynamic body! Move this body via the entity's transform component."); + return; + } + + JoltScene::GetBodyInterface().SetLinearVelocity(m_BodyID, JoltUtils::ToJoltVector(inVelocity)); + } + + glm::vec3 JoltBody::GetAngularVelocity() const + { + if (IsStatic()) + { + return glm::vec3(0.0f); + } + + return JoltUtils::FromJoltVector(JoltScene::GetBodyInterface().GetAngularVelocity(m_BodyID)); + } + + void JoltBody::SetAngularVelocity(const glm::vec3& inVelocity) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set angular velocity on a non-dynamic body! Rotate this body via the entity's transform component."); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + auto& body = lock.GetBody(); + auto velocity = JoltUtils::ToJoltVector(inVelocity); + body.SetAngularVelocityClamped(velocity); + + if (!body.IsActive() && !velocity.IsNearZero() && body.IsInBroadPhase()) + JoltScene::GetBodyInterface(false).ActivateBody(m_BodyID); + } + + float JoltBody::GetMaxLinearVelocity() const + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot get max linear velocity on a non-dynamic body!"); + return 0.0f; + } + + JPH::BodyLockRead lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + return lock.GetBody().GetMotionProperties()->GetMaxLinearVelocity(); + } + + void JoltBody::SetMaxLinearVelocity(float inMaxVelocity) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set max linear velocity on a non-dynamic body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + lock.GetBody().GetMotionProperties()->SetMaxLinearVelocity(inMaxVelocity); + } + + float JoltBody::GetMaxAngularVelocity() const + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot get max angular velocity on a non-dynamic body!"); + return 0.0f; + } + + JPH::BodyLockRead lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + return lock.GetBody().GetMotionProperties()->GetMaxAngularVelocity(); + } + + void JoltBody::SetMaxAngularVelocity(float inMaxVelocity) + { + if (!IsDynamic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set max angular velocity on a non-dynamic body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + lock.GetBody().GetMotionProperties()->SetMaxAngularVelocity(inMaxVelocity); + } + + bool JoltBody::IsSleeping() const + { + if (IsStatic()) + { + return false; + } + + JPH::BodyLockRead lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + return !lock.GetBody().IsActive(); + } + + void JoltBody::SetSleepState(bool inSleep) + { + if (IsStatic()) + { + SE_CONSOLE_LOG_ERROR("Cannot change sleep state of a static body!"); + return; + } + + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + + if (!lock.Succeeded()) + return; + + auto& body = lock.GetBody(); + + if (inSleep) + { + JoltScene::GetBodyInterface(false).DeactivateBody(m_BodyID); + } + else if (body.IsInBroadPhase()) + { + JoltScene::GetBodyInterface(false).ActivateBody(m_BodyID); + } + } + + + void JoltBody::SetCollisionDetectionMode(ECollisionDetectionType collisionDetectionMode) + { + JoltScene::GetBodyInterface().SetMotionQuality(m_BodyID, JoltUtils::ToJoltMotionQuality(collisionDetectionMode)); + } + + + glm::vec3 JoltBody::GetTranslation() const + { + return JoltUtils::FromJoltVector(JoltScene::GetBodyInterface().GetPosition(m_BodyID)); + } + + + glm::quat JoltBody::GetRotation() const + { + return JoltUtils::FromJoltQuat(JoltScene::GetBodyInterface().GetRotation(m_BodyID)); + } + + + void JoltBody::SetTranslation(const glm::vec3& translation) + { + JPH::BodyLockWrite bodyLock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(bodyLock.Succeeded()); + JPH::Body& body = bodyLock.GetBody(); + + if (!body.IsStatic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set translation on a non-static body!"); + return; + } + + JoltScene::GetBodyInterface(false).SetPosition(m_BodyID, JoltUtils::ToJoltVector(translation), JPH::EActivation::DontActivate); + + // Make sure this change takes effect in the entity TransformComponent + auto entityScene = Scene::GetScene(m_Entity.GetSceneUUID()); + entityScene->GetPhysicsScene()->MarkForSynchronization(this); + } + + + void JoltBody::SetRotation(const glm::quat& rotation) + { + JPH::BodyLockWrite bodyLock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(bodyLock.Succeeded()); + JPH::Body& body = bodyLock.GetBody(); + + if (!body.IsStatic()) + { + SE_CONSOLE_LOG_ERROR("Cannot set rotation on a non-static body!"); + return; + } + + JoltScene::GetBodyInterface(false).SetRotation(m_BodyID, JoltUtils::ToJoltQuat(rotation), JPH::EActivation::DontActivate); + + // Make sure this change takes effect in the entity TransformComponent + auto entityScene = Scene::GetScene(m_Entity.GetSceneUUID()); + entityScene->GetPhysicsScene()->MarkForSynchronization(this); + } + + + void JoltBody::OnAxisLockUpdated(bool forceWake) + { + JPH::BodyLockWrite bodyLock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(bodyLock.Succeeded()); + + if (m_AxisLockConstraint) + { + JoltScene::GetJoltSystem().RemoveConstraint(m_AxisLockConstraint); + m_AxisLockConstraint = nullptr; + } + + // Don't recreate if we have no locked axes + if (m_LockedAxes != EActorAxis::None) + CreateAxisLockConstraint(bodyLock.GetBody()); + } + + void JoltBody::CreateStaticBody(JPH::BodyInterface& bodyInterface) + { + Ref scene = Scene::GetScene(m_Entity.GetSceneUUID()); + SE_CORE_VERIFY(scene, "No scene active?"); + + const TransformComponent worldTransform = scene->GetWorldSpaceTransform(m_Entity); + const auto& rigidBodyComponent = m_Entity.GetComponent(); + + CreateCollisionShapesForEntity(m_Entity); + + if (m_Shapes.empty()) + return; + + JPH::Shape* firstShape = nullptr; + + if (m_Shapes.find(ShapeType::CompoundShape) != m_Shapes.end()) + { + firstShape = static_cast(m_Shapes.at(ShapeType::CompoundShape)[0]->GetNativeShape()); + } + else if (m_Shapes.find(ShapeType::MutableCompoundShape) != m_Shapes.end()) + { + firstShape = static_cast(m_Shapes.at(ShapeType::MutableCompoundShape)[0]->GetNativeShape()); + } + else + { + for (const auto& [shapeType, shapeStorage] : m_Shapes) + { + firstShape = static_cast(shapeStorage[0]->GetNativeShape()); + break; + } + } + + if (firstShape == nullptr) + { + SE_CORE_INFO_TAG("Physics", "Failed to create static PhysicsBody, no collision shape provided!"); + m_Shapes.clear(); + return; + } + + JPH::BodyCreationSettings bodySettings( + firstShape, + JoltUtils::ToJoltVector(worldTransform.Translation), + JoltUtils::ToJoltQuat(glm::normalize(worldTransform.GetRotation())), + JPH::EMotionType::Static, + JPH::ObjectLayer(rigidBodyComponent.LayerID) + ); + bodySettings.mIsSensor = rigidBodyComponent.IsTrigger; + bodySettings.mAllowDynamicOrKinematic = rigidBodyComponent.EnableDynamicTypeChange; + bodySettings.mAllowSleeping = false; + + JPH::Body* body = bodyInterface.CreateBody(bodySettings); + + if (body == nullptr) + { + SE_CORE_ERROR_TAG("Physics", "Failed to create PhysicsBody! This means we don't have enough space to store more RigidBodies!"); + return; + } + + body->SetUserData(m_Entity.GetUUID()); + m_BodyID = body->GetID(); + } + + void JoltBody::CreateDynamicBody(JPH::BodyInterface& bodyInterface) + { + Ref scene = Scene::GetScene(m_Entity.GetSceneUUID()); + SE_CORE_VERIFY(scene, "No scene active?"); + + const TransformComponent worldTransform = scene->GetWorldSpaceTransform(m_Entity); + auto& rigidBodyComponent = m_Entity.GetComponent(); + + CreateCollisionShapesForEntity(m_Entity); + + if (m_Shapes.empty()) + return; + + JPH::Shape* firstShape = nullptr; + + if (m_Shapes.find(ShapeType::CompoundShape) != m_Shapes.end()) + { + firstShape = static_cast(m_Shapes.at(ShapeType::CompoundShape)[0]->GetNativeShape()); + } + else if (m_Shapes.find(ShapeType::MutableCompoundShape) != m_Shapes.end()) + { + firstShape = static_cast(m_Shapes.at(ShapeType::MutableCompoundShape)[0]->GetNativeShape()); + } + else + { + for (const auto& [shapeType, shapeStorage] : m_Shapes) + { + firstShape = static_cast(shapeStorage[0]->GetNativeShape()); + break; + } + } + + if (firstShape == nullptr) + { + SE_CORE_INFO_TAG("Physics", "Failed to create dynamic PhysicsBody, no collision shape provided!"); + m_Shapes.clear(); + return; + } + + if (!PhysicsLayerManager::IsLayerValid(rigidBodyComponent.LayerID)) + { + rigidBodyComponent.LayerID = 0; // Use the default layer + SE_CONSOLE_LOG_WARN("Entity '{}' has a RigidBodyComponent with an invalid layer set! Using the Default layer.", m_Entity.Name()); + } + + JPH::BodyCreationSettings bodySettings( + firstShape, + JoltUtils::ToJoltVector(worldTransform.Translation), + JoltUtils::ToJoltQuat(glm::normalize(worldTransform.GetRotation())), + JoltUtils::ToJoltMotionType(rigidBodyComponent.BodyType), + JPH::ObjectLayer(rigidBodyComponent.LayerID) + ); + bodySettings.mLinearDamping = rigidBodyComponent.LinearDrag; + bodySettings.mAngularDamping = rigidBodyComponent.AngularDrag; + bodySettings.mIsSensor = rigidBodyComponent.IsTrigger; + bodySettings.mGravityFactor = rigidBodyComponent.DisableGravity ? 0.0f : 1.0f; + bodySettings.mLinearVelocity = JoltUtils::ToJoltVector(rigidBodyComponent.InitialLinearVelocity); + bodySettings.mAngularVelocity = JoltUtils::ToJoltVector(rigidBodyComponent.InitialAngularVelocity); + bodySettings.mMaxLinearVelocity = rigidBodyComponent.MaxLinearVelocity; + bodySettings.mMaxAngularVelocity = rigidBodyComponent.MaxAngularVelocity; + bodySettings.mMotionQuality = JoltUtils::ToJoltMotionQuality(rigidBodyComponent.CollisionDetection); + bodySettings.mAllowSleeping = true; + + JPH::Body* body = bodyInterface.CreateBody(bodySettings); + if (body == nullptr) + { + SE_CORE_ERROR_TAG("Physics", "Failed to create PhysicsBody! This means we don't have enough space to store more RigidBodies!"); + return; + } + + body->SetUserData(m_Entity.GetUUID()); + m_BodyID = body->GetID(); + + // Only create the constraint if it's actually needed + if (rigidBodyComponent.LockedAxes != EActorAxis::None) + CreateAxisLockConstraint(*body); + } + + void JoltBody::CreateAxisLockConstraint(JPH::Body& body) + { + JPH::SixDOFConstraintSettings constraintSettings; + constraintSettings.mPosition1 = constraintSettings.mPosition2 = body.GetCenterOfMassPosition(); + + if ((m_LockedAxes & EActorAxis::TranslationX) != EActorAxis::None) + constraintSettings.MakeFixedAxis(JPH::SixDOFConstraintSettings::TranslationX); + + if ((m_LockedAxes & EActorAxis::TranslationY) != EActorAxis::None) + constraintSettings.MakeFixedAxis(JPH::SixDOFConstraintSettings::TranslationY); + + if ((m_LockedAxes & EActorAxis::TranslationZ) != EActorAxis::None) + constraintSettings.MakeFixedAxis(JPH::SixDOFConstraintSettings::TranslationZ); + + if ((m_LockedAxes & EActorAxis::RotationX) != EActorAxis::None) + constraintSettings.MakeFixedAxis(JPH::SixDOFConstraintSettings::RotationX); + + if ((m_LockedAxes & EActorAxis::RotationY) != EActorAxis::None) + constraintSettings.MakeFixedAxis(JPH::SixDOFConstraintSettings::RotationY); + + if ((m_LockedAxes & EActorAxis::RotationZ) != EActorAxis::None) + constraintSettings.MakeFixedAxis(JPH::SixDOFConstraintSettings::RotationZ); + + m_AxisLockConstraint = (JPH::SixDOFConstraint*)constraintSettings.Create(JPH::Body::sFixedToWorld, body); + + JoltScene::GetJoltSystem().AddConstraint(m_AxisLockConstraint); + } + + void JoltBody::Release() + { + JPH::BodyLockWrite lock(JoltScene::GetBodyLockInterface(), m_BodyID); + SE_CORE_VERIFY(lock.Succeeded()); + auto& body = lock.GetBody(); + + if (m_AxisLockConstraint != nullptr) + { + JoltScene::GetJoltSystem().RemoveConstraint(m_AxisLockConstraint); + m_AxisLockConstraint = nullptr; + } + + if (body.IsInBroadPhase()) + JoltScene::GetBodyInterface(false).RemoveBody(m_BodyID); + else + JoltScene::GetBodyInterface(false).DeactivateBody(m_BodyID); + + m_Shapes.clear(); + JoltScene::GetBodyInterface(false).DestroyBody(m_BodyID); + } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBody.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBody.h new file mode 100644 index 00000000..9a36759d --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltBody.h @@ -0,0 +1,90 @@ +#pragma once + +#include "StarEngine/Physics/PhysicsBody.h" + +#include +#include + +namespace StarEngine { + + class JoltBody : public PhysicsBody + { + public: + JoltBody(JPH::BodyInterface& bodyInterface, Entity entity); + ~JoltBody(); + + virtual void SetCollisionLayer(uint32_t layerID) override; + + virtual bool IsStatic() const override; + virtual bool IsDynamic() const override; + virtual bool IsKinematic() const override; + + virtual void MoveKinematic(const glm::vec3& targetPosition, const glm::quat& targetRotation, float deltaSeconds) override; + virtual void Rotate(const glm::vec3& inRotationTimesDeltaTime) override; + + virtual void SetGravityEnabled(bool isEnabled) override; + virtual void AddForce(const glm::vec3& force, EForceMode forceMode = EForceMode::Force, bool forceWake = true) override; + virtual void AddForce(const glm::vec3& force, const glm::vec3& location, EForceMode forceMode = EForceMode::Force, bool forceWake = true) override; + virtual void AddTorque(const glm::vec3& torque, bool forceWake = true) override; + virtual void AddRadialImpulse(const glm::vec3& origin, float radius, float strength, EFalloffMode falloff, bool velocityChange) override; + + virtual void ChangeTriggerState(bool isTrigger) override; + virtual bool IsTrigger() const override; + + virtual float GetMass() const override; + virtual void SetMass(float mass) override; + + virtual void SetLinearDrag(float inLinearDrag) override; + virtual void SetAngularDrag(float inAngularDrag) override; + + virtual glm::vec3 GetLinearVelocity() const override; + virtual void SetLinearVelocity(const glm::vec3& inVelocity) override; + + virtual glm::vec3 GetAngularVelocity() const override; + virtual void SetAngularVelocity(const glm::vec3& inVelocity) override; + + virtual float GetMaxLinearVelocity() const override; + virtual void SetMaxLinearVelocity(float inMaxVelocity) override; + + virtual float GetMaxAngularVelocity() const override; + virtual void SetMaxAngularVelocity(float inMaxVelocity) override; + + virtual bool IsSleeping() const override; + virtual void SetSleepState(bool inSleep) override; + + virtual void SetCollisionDetectionMode(ECollisionDetectionType collisionDetectionMode) override; + + JPH::BodyID GetBodyID() const { return m_BodyID; } + + virtual glm::vec3 GetTranslation() const override; + virtual glm::quat GetRotation() const override; + + virtual void SetTranslation(const glm::vec3& translation) override; + virtual void SetRotation(const glm::quat& rotation) override; + + private: + virtual void OnAxisLockUpdated(bool forceWake) override; + + private: + void CreateStaticBody(JPH::BodyInterface& bodyInterface); + void CreateDynamicBody(JPH::BodyInterface& bodyInterface); + + void CreateAxisLockConstraint(JPH::Body& body); + + void Release(); + + private: + JPH::BodyID m_BodyID; + JPH::EMotionType m_OldMotionType = JPH::EMotionType::Static; + + glm::vec3 m_KinematicTargetPosition = { 0.0f, 0.0f, 0.0f }; + glm::quat m_KinematicTargetRotation = { 0.0f, 0.0f, 0.0f, 1.0f }; + float m_KinematicTargetTime = 0.0f; + + JPH::SixDOFConstraint* m_AxisLockConstraint = nullptr; + + private: + friend class JoltScene; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCaptureManager.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCaptureManager.cpp new file mode 100644 index 00000000..76a288fd --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCaptureManager.cpp @@ -0,0 +1,85 @@ +#include "sepch.h" +#include "JoltCaptureManager.h" +#include "JoltScene.h" + +#include "StarEngine/Utilities/FileSystem.h" +#include "StarEngine/Utilities/StringUtils.h" +#include "StarEngine/Utilities/ProcessHelper.h" + +namespace StarEngine { + +#ifndef SE_DIST + void JoltCaptureOutStream::Open(const std::filesystem::path& inPath) + { + SE_CORE_VERIFY(!m_Stream.is_open()); + m_Stream.open(inPath, std::ofstream::out | std::ofstream::binary | std::ofstream::trunc); + } + + void JoltCaptureOutStream::Close() + { + if (m_Stream.is_open()) + m_Stream.close(); + } + + void JoltCaptureOutStream::WriteBytes(const void* inData, size_t inNumBytes) + { + m_Stream.write((const char*)inData, inNumBytes); + } + + bool JoltCaptureOutStream::IsFailed() const { return m_Stream.fail(); } + +#endif + + JoltCaptureManager::JoltCaptureManager() + : PhysicsCaptureManager() + { + } + +#ifndef SE_DIST + + void JoltCaptureManager::BeginCapture() + { + if (IsCapturing()) + return; + + m_RecentCapture = m_CapturesDirectory / ("Capture-" + Utils::String::GetCurrentTimeString(true, true) + ".jor"); + m_Captures.push_back(m_RecentCapture); + m_Stream.Open(m_RecentCapture); + m_Recorder = std::make_unique(m_Stream); + } + + void JoltCaptureManager::CaptureFrame() + { + if (!IsCapturing()) + return; + + JoltScene::GetJoltSystem().DrawBodies(m_DrawSettings, m_Recorder.get()); + m_Recorder->EndFrame(); + } + + void JoltCaptureManager::EndCapture() + { + if (!IsCapturing()) + return; + + m_Stream.Close(); + m_Recorder.reset(); + } + +#endif + + void JoltCaptureManager::OpenCapture(const std::filesystem::path& capturePath) const + { + if (!FileSystem::Exists(capturePath)) + return; + + ProcessInfo joltViewerProcessInfo; + joltViewerProcessInfo.FilePath = std::filesystem::path("Tools") / "JoltViewer" / "JoltViewer.exe"; + joltViewerProcessInfo.CommandLine = std::filesystem::absolute(capturePath).string(); + joltViewerProcessInfo.WorkingDirectory = ""; + joltViewerProcessInfo.Detached = true; + ProcessHelper::CreateProcess(joltViewerProcessInfo); + } + +} + diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCaptureManager.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCaptureManager.h new file mode 100644 index 00000000..b7d33133 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCaptureManager.h @@ -0,0 +1,60 @@ +#pragma once + +#include "StarEngine/Physics/PhysicsCaptureManager.h" + +#ifndef SE_DIST + #include + #include + #include +#endif + +namespace StarEngine { + +#ifndef SE_DIST + class JoltCaptureOutStream : public JPH::StreamOut + { + public: + void Open(const std::filesystem::path& inPath); + void Close(); + bool IsOpen() const { return m_Stream.is_open(); } + + virtual void WriteBytes(const void* inData, size_t inNumBytes) override; + virtual bool IsFailed() const override; + + private: + std::ofstream m_Stream; + }; + + class JoltCaptureManager : public PhysicsCaptureManager + { + public: + JoltCaptureManager(); + + virtual void BeginCapture() override; + virtual void CaptureFrame() override; + virtual void EndCapture() override; + virtual bool IsCapturing() const override { return m_Stream.IsOpen() && m_Recorder != nullptr; } + virtual void OpenCapture(const std::filesystem::path& capturePath) const override; + + private: + JoltCaptureOutStream m_Stream; + std::unique_ptr m_Recorder = nullptr; + JPH::BodyManager::DrawSettings m_DrawSettings; + }; + +#else + + class JoltCaptureManager : public PhysicsCaptureManager + { + public: + JoltCaptureManager(); + + virtual void BeginCapture() override {} + virtual void CaptureFrame() override {} + virtual void EndCapture() override {} + virtual bool IsCapturing() const override { return false; } + virtual void OpenCapture(const std::filesystem::path& capturePath) const override; + }; + +#endif +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCharacterController.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCharacterController.cpp new file mode 100644 index 00000000..49b61181 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCharacterController.cpp @@ -0,0 +1,351 @@ +#include "sepch.h" +#include "JoltCharacterController.h" +#include "JoltUtils.h" +#include "JoltAPI.h" +#include "JoltScene.h" +#include "JoltMaterial.h" + +#include "StarEngine/Physics/PhysicsSystem.h" +#include "StarEngine/Scripting/ScriptEngine.h" + +#include +#include +#include + +#ifdef JPH_DEBUG_RENDERER +#include +#endif + +#include "magic_enum.hpp" +using namespace magic_enum::bitwise_operators; + +namespace StarEngine { + + JoltCharacterController::JoltCharacterController(Entity entity, const ContactCallbackFn& contactCallback) + : m_Entity(entity), m_ContactEventCallback(contactCallback) + { + Create(); + } + + JoltCharacterController::~JoltCharacterController() + { + // Make sure controller is destroyed before the rest of the class (in particular before m_Shape) + m_Controller = nullptr; + } + + void JoltCharacterController::SetSlopeLimit(float slopeLimit) + { + m_Controller->SetMaxSlopeAngle(glm::radians(slopeLimit)); + } + + void JoltCharacterController::SetStepOffset(float stepOffset) + { + m_StepOffset = stepOffset; + } + + void JoltCharacterController::SetTranslation(const glm::vec3& inTranslation) + { + m_Controller->SetPosition(JoltUtils::ToJoltVector(inTranslation)); + } + + void JoltCharacterController::SetRotation(const glm::quat& inRotation) + { + m_Controller->SetRotation(JoltUtils::ToJoltQuat(inRotation)); + } + + bool JoltCharacterController::IsGrounded() const + { + return m_Controller->IsSupported(); + } + + void JoltCharacterController::Move(const glm::vec3& displacement) + { + if (m_Controller->IsSupported() || m_ControlMovementInAir) + { + m_Displacement += JoltUtils::ToJoltVector(displacement); + } + } + + void JoltCharacterController::Rotate(const glm::quat& rotation) + { + // avoid quat multiplication if rotation is close to identity + if ((m_Controller->IsSupported() || m_ControlRotationInAir) && fabs(rotation.w - 1.0f) > 0.000001) + { + m_Rotation = m_Rotation * JoltUtils::ToJoltQuat(rotation); + } + } + + void JoltCharacterController::Jump(float jumpPower) + { + m_JumpPower = jumpPower; + } + + glm::vec3 JoltCharacterController::GetLinearVelocity() const + { + return m_Controller->GetGroundState() == JPH::CharacterVirtual::EGroundState::OnGround ? glm::zero() : JoltUtils::FromJoltVector(m_Controller->GetLinearVelocity()); + } + + void JoltCharacterController::SetLinearVelocity(const glm::vec3& linearVelocity) + { + m_Controller->SetLinearVelocity(JoltUtils::ToJoltVector(linearVelocity)); + } + + void JoltCharacterController::PreSimulate(float deltaTime) + { + if (deltaTime <= 0.0f) + return; + + m_DesiredVelocity = m_Displacement / deltaTime; + m_Controller->SetRotation((m_Controller->GetRotation() * m_Rotation).Normalized()); // re-normalize to avoid accumulating errors + m_AllowSliding = !m_Controller->IsSupported() || !m_Displacement.IsNearZero(); + + m_Controller->UpdateGroundVelocity(); + + JPH::Vec3 currentVerticalVelocity = JPH::Vec3(0, m_Controller->GetLinearVelocity().GetY(), 0); + JPH::Vec3 groundVelocity = m_Controller->GetGroundVelocity(); + + bool jumping = (currentVerticalVelocity.GetY() - groundVelocity.GetY()) >= 0.1f; + + JPH::Vec3 newVelocity; + if (IsGravityEnabled()) + { + if (m_Controller->GetGroundState() == JPH::CharacterVirtual::EGroundState::OnGround && (!m_Controller->IsSlopeTooSteep(m_Controller->GetGroundNormal()))) + { + // When grounded, acquire velocity of ground + newVelocity = groundVelocity; + + // Jump + if (m_JumpPower > 0.0f && !jumping) + { + newVelocity += JPH::Vec3(0, m_JumpPower, 0); + m_JumpPower = 0.0f; + } + } + else + { + newVelocity = currentVerticalVelocity; + } + + // Add gravity + newVelocity += JoltUtils::ToJoltVector(PhysicsSystem::GetSettings().Gravity) * deltaTime; + } + else + { + newVelocity = JPH::Vec3::sZero(); + } + + if (m_Controller->IsSupported() || m_ControlMovementInAir) + { + newVelocity += m_DesiredVelocity; + } + else + { + // preserve current horizontal velocity + JPH::Vec3 currentHorizontalVelocity = m_Controller->GetLinearVelocity() - currentVerticalVelocity; + newVelocity += currentHorizontalVelocity; + } + + // Update the velocity + m_Controller->SetLinearVelocity(newVelocity); + + // TODO (0x): Collider switch if, for example, player changes stance + } + + + void JoltCharacterController::Simulate(float deltaTime) + { + JPH::Vec3 gravity = JoltUtils::ToJoltVector(PhysicsSystem::GetSettings().Gravity); + const auto& characterControllerComponent = m_Entity.GetComponent(); + + auto broadPhaseLayerFilter = JoltScene::GetJoltSystem().GetDefaultBroadPhaseLayerFilter(JPH::ObjectLayer(characterControllerComponent.LayerID)); + auto layerFilter = JoltScene::GetJoltSystem().GetDefaultLayerFilter(JPH::ObjectLayer(characterControllerComponent.LayerID)); + auto* tempAllocator = static_cast(PhysicsSystem::GetAPI())->GetTempAllocator(); + +#ifdef JPH_DEBUG_RENDERER + m_Controller->GetShape()->Draw(JPH::DebugRenderer::sInstance, m_Controller->GetCenterOfMassTransform(), JPH::Vec3::sReplicate(1.0f), JPH::Color::sGreen, false, true); +#endif + + m_Controller->ExtendedUpdate(deltaTime, gravity, { .mWalkStairsStepUp = {0.0f , m_StepOffset, 0.0f }, .mWalkStairsStepForwardTest = m_Controller->GetShape()->GetInnerRadius() }, broadPhaseLayerFilter, layerFilter, {}, {}, *tempAllocator); + } + + + void JoltCharacterController::PostSimulate() + { + if (m_Controller->IsSupported() || m_ControlMovementInAir) + { + m_Displacement = JPH::Vec3::sZero(); + } + if(m_Controller->IsSupported() || m_ControlRotationInAir) { + m_Rotation = JPH::Quat::sIdentity(); + } + + // raise "end" events for bodies in m_TriggeredBodies or m_CollidedBodies lists that are no longer in the m_StillTriggeredBodies and m_StillCollidedBodies lists + Ref scene = ScriptEngine::GetInstance().GetCurrentScene(); + if (scene->IsPlaying()) + { + for (const auto& bodyID : m_TriggeredBodies) + { + if (std::find(m_StillTriggeredBodies.begin(), m_StillTriggeredBodies.end(), bodyID) == m_StillTriggeredBodies.end()) + { + UUID otherEntityID = JoltScene::GetBodyInterface().GetUserData(bodyID); + m_ContactEventCallback(ContactType::TriggerEnd, m_Entity.GetUUID(), otherEntityID); + } + } + + for (const auto& bodyID : m_CollidedBodies) + { + if (std::find(m_StillCollidedBodies.begin(), m_StillCollidedBodies.end(), bodyID) == m_StillCollidedBodies.end()) + { + UUID otherEntityID = JoltScene::GetBodyInterface().GetUserData(bodyID); + m_ContactEventCallback(ContactType::CollisionEnd, m_Entity.GetUUID(), otherEntityID); + } + } + + std::swap(m_TriggeredBodies, m_StillTriggeredBodies); + std::swap(m_CollidedBodies, m_StillCollidedBodies); + + m_StillTriggeredBodies.clear(); + m_StillCollidedBodies.clear(); + } + } + + + void JoltCharacterController::Create() + { + Ref scene = Scene::GetScene(m_Entity.GetSceneUUID()); + SE_CORE_VERIFY(scene, "No scene active?"); + + const auto transformComponent = scene->GetWorldSpaceTransform(m_Entity); + const auto& characterControllerComponent = m_Entity.GetComponent(); + + m_CollisionLayer = characterControllerComponent.LayerID; + + JPH::Ref settings = new JPH::CharacterVirtualSettings(); + settings->mMaxSlopeAngle = glm::radians(characterControllerComponent.SlopeLimitDeg); + m_StepOffset = characterControllerComponent.StepOffset; + + if (m_Entity.HasComponent()) + { + m_Shape = BoxShape::Create(m_Entity, 100.0f); + } + else if (m_Entity.HasComponent()) + { + m_Shape = CapsuleShape::Create(m_Entity, 100.0f); + } + else if (m_Entity.HasComponent()) + { + m_Shape = SphereShape::Create(m_Entity, 100.0f); + } + + settings->mShape = static_cast(m_Shape->GetNativeShape()); + + // Note (0x): InnerBodyShape is required in order that character controllers will register collisions with other character controllers + // There are other possible ways of doing this (e.g. JPH::CharacterVsCharacterCollision interface). + settings->mInnerBodyShape = settings->mShape; + + m_Controller = new JPH::CharacterVirtual(settings, JoltUtils::ToJoltVector(transformComponent.Translation), JoltUtils::ToJoltQuat(transformComponent.GetRotation()), &JoltScene::GetJoltSystem()); + m_Controller->SetListener(this); + auto& bi = JoltScene::GetBodyInterface(false); + bi.SetUserData(m_Controller->GetInnerBodyID(), m_Entity.GetUUID()); + bi.SetObjectLayer(m_Controller->GetInnerBodyID(), m_CollisionLayer); + m_HasGravity = !characterControllerComponent.DisableGravity; + m_ControlMovementInAir = characterControllerComponent.ControlMovementInAir; + m_ControlRotationInAir = characterControllerComponent.ControlRotationInAir; + } + + + void JoltCharacterController::HandleTrigger(Ref scene, const JPH::BodyID bodyID2) + { + if (std::find(m_TriggeredBodies.begin(), m_TriggeredBodies.end(), bodyID2) == m_TriggeredBodies.end()) + { + m_ContactEventCallback(ContactType::TriggerBegin, m_Entity.GetUUID(), JoltScene::GetBodyInterface(false).GetUserData(bodyID2)); + } + m_StillTriggeredBodies.push_back(bodyID2); + } + + + void JoltCharacterController::HandleCollision(Ref scene, const JPH::BodyID bodyID2) + { + if (std::find(m_CollidedBodies.begin(), m_CollidedBodies.end(), bodyID2) == m_CollidedBodies.end()) + { + m_ContactEventCallback(ContactType::CollisionBegin, m_Entity.GetUUID(), JoltScene::GetBodyInterface(false).GetUserData(bodyID2)); + } + m_StillCollidedBodies.push_back(bodyID2); + } + + + void JoltCharacterController::OnAdjustBodyVelocity(const JPH::CharacterVirtual* inCharacter, const JPH::Body& inBody2, JPH::Vec3& ioLinearVelocity, JPH::Vec3& ioAngularVelocity) + { + // TODO (0x): marshal this call out to game play, and get result back again as appropriate for contacted inBody2 + } + + + bool JoltCharacterController::OnContactValidate(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2) + { + auto objectLayer = JoltScene::GetBodyInterface().GetObjectLayer(inBodyID2); + const auto& characterLayer = PhysicsLayerManager::GetLayer(/*m_CharacterController->*/m_CollisionLayer); + const auto& bodyLayer = PhysicsLayerManager::GetLayer(objectLayer); + + if (characterLayer.LayerID == bodyLayer.LayerID) + return characterLayer.CollidesWithSelf; + + return characterLayer.CollidesWith & bodyLayer.BitValue; + } + + + void JoltCharacterController::OnContactAdded(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2, JPH::Vec3Arg inContactPosition, JPH::Vec3Arg inContactNormal, JPH::CharacterContactSettings& ioSettings) + { + m_CollisionFlags = ECollisionFlags::None; + + // for now, any dynamic body can push the character. TODO (0x): add settings to control this + bool isSensor = false; + bool isStatic = true; + JPH::BodyLockRead lock(JoltScene::GetBodyLockInterface(), inBodyID2); + if (lock.Succeeded()) + { + isSensor = lock.GetBody().IsSensor(); + isStatic = lock.GetBody().IsStatic(); + } + + ioSettings.mCanPushCharacter = !isSensor && !isStatic; + + if (inContactNormal.GetY() < 0.0f) + m_CollisionFlags |= ECollisionFlags::Below; + else if (inContactNormal.GetY() > 0.0f) + m_CollisionFlags |= ECollisionFlags::Above; + + if (inContactNormal.GetX() != 0.0f || inContactNormal.GetZ() != 0.0f) + m_CollisionFlags |= ECollisionFlags::Sides; + + // If we encounter an object that can push us, enable sliding + if (ioSettings.mCanPushCharacter) + { + m_AllowSliding = true; + } + + Ref scene = ScriptEngine::GetInstance().GetCurrentScene(); + + if (!scene->IsPlaying()) + return; + + if (isSensor) + { + HandleTrigger(scene, inBodyID2); + } + else + { + HandleCollision(scene, inBodyID2); + } + } + + + void JoltCharacterController::OnContactSolve(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2, JPH::RVec3Arg inContactPosition, JPH::Vec3Arg inContactNormal, JPH::Vec3Arg inContactVelocity, const JPH::PhysicsMaterial* inContactMaterial, JPH::Vec3Arg inCharacterVelocity, JPH::Vec3& ioNewCharacterVelocity) + { + // Don't allow the player to slide down static not-too-steep surfaces unless they are actively moving + if (!m_AllowSliding && inContactVelocity.IsNearZero() && !inCharacter->IsSlopeTooSteep(inContactNormal)) + { + ioNewCharacterVelocity = JPH::Vec3::sZero(); + } + } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCharacterController.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCharacterController.h new file mode 100644 index 00000000..1e82227f --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCharacterController.h @@ -0,0 +1,95 @@ +#pragma once + +#include "StarEngine/Physics/CharacterController.h" +#include "StarEngine/Physics/PhysicsContactCallback.h" +#include "StarEngine/Physics/PhysicsShapes.h" + +#include +#include +#include +#include +#include + +namespace StarEngine { + + class JoltCharacterController : public CharacterController, JPH::CharacterContactListener + { + public: + JoltCharacterController(Entity entity, const ContactCallbackFn& contactCallback); + ~JoltCharacterController(); + + virtual void SetGravityEnabled(bool enableGravity) override { m_HasGravity = enableGravity; } + virtual bool IsGravityEnabled() const override { return m_HasGravity; } + + virtual void SetSlopeLimit(float slopeLimit) override; + virtual void SetStepOffset(float stepOffset) override; + + virtual void SetTranslation(const glm::vec3& inTranslation) override; + virtual void SetRotation(const glm::quat& inRotation) override; + + virtual bool IsGrounded() const override; + + virtual void SetControlMovementInAir(bool controlMovementInAir) override { m_ControlMovementInAir = controlMovementInAir; } + virtual bool CanControlMovementInAir() const override { return m_ControlMovementInAir; } + virtual void SetControlRotationInAir(bool controlRotationInAir) override { m_ControlRotationInAir = controlRotationInAir; } + virtual bool CanControlRotationInAir() const override { return m_ControlRotationInAir; } + + virtual ECollisionFlags GetCollisionFlags() const override { return m_CollisionFlags; } + + virtual void Move(const glm::vec3& displacement) override; + virtual void Rotate(const glm::quat& rotation) override; + virtual void Jump(float jumpPower) override; + + virtual glm::vec3 GetLinearVelocity() const override; + virtual void SetLinearVelocity(const glm::vec3& linearVelocity) override; + + JPH::BodyID GetBodyID() const { return m_Controller->GetInnerBodyID(); } + + private: + virtual void PreSimulate(float deltaTime) override; + virtual void Simulate(float deltaTime) override; + virtual void PostSimulate() override; + + void Create(); + + // JPH::CharacterContactListener + void OnAdjustBodyVelocity(const JPH::CharacterVirtual* inCharacter, const JPH::Body& inBody2, JPH::Vec3& ioLinearVelocity, JPH::Vec3& ioAngularVelocity) override; + bool OnContactValidate(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2) override; + void OnContactAdded(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2, JPH::Vec3Arg inContactPosition, JPH::Vec3Arg inContactNormal, JPH::CharacterContactSettings& ioSettings) override; + void OnContactSolve(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2, JPH::RVec3Arg inContactPosition, JPH::Vec3Arg inContactNormal, JPH::Vec3Arg inContactVelocity, const JPH::PhysicsMaterial* inContactMaterial, JPH::Vec3Arg inCharacterVelocity, JPH::Vec3& ioNewCharacterVelocity) override; + + void HandleTrigger(Ref scene, const JPH::BodyID bodyID2); + void HandleCollision(Ref scene, const JPH::BodyID bodyID2); + + private: + JPH::BodyIDVector m_TriggeredBodies; + JPH::BodyIDVector m_StillTriggeredBodies; + JPH::BodyIDVector m_CollidedBodies; + JPH::BodyIDVector m_StillCollidedBodies; + + JPH::Quat m_Rotation = JPH::Quat::sIdentity(); // rotation (if any) to be applied during next physics update (comes from Rotate() calls) + JPH::Vec3 m_Displacement = JPH::Vec3::sZero(); // displacement (if any) to be applied during next physics update (comes from Move() calls) + JPH::Vec3 m_DesiredVelocity = JPH::Vec3::sZero(); // desired velocity for next physics update is calculated during PreSimulate() + + Entity m_Entity; + + ContactCallbackFn m_ContactEventCallback; + + JPH::Ref m_Controller; + Ref m_Shape; + + float m_JumpPower = 0.0f; + float m_StepOffset = 0.0f; + + uint32_t m_CollisionLayer; + ECollisionFlags m_CollisionFlags = ECollisionFlags::None; + + bool m_HasGravity = true; + bool m_ControlMovementInAir = false; + bool m_ControlRotationInAir = false; + bool m_AllowSliding = false; + + private: + friend class JoltScene; + }; +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltContactListener.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltContactListener.cpp new file mode 100644 index 00000000..02e7f07f --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltContactListener.cpp @@ -0,0 +1,100 @@ +#include "sepch.h" +#include "JoltContactListener.h" +#include "JoltMaterial.h" + +#include "StarEngine/Scripting/ScriptEngine.h" + +namespace StarEngine { + + void JoltContactListener::OnContactAdded(const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings) + { + Ref scene = ScriptEngine::GetInstance().GetCurrentScene(); + + if (!scene->IsPlaying()) + return; + + OverrideFrictionAndRestitution(inBody1, inBody2, inManifold, ioSettings); + + if (ioSettings.mIsSensor) + OnTriggerBegin(scene, inBody1, inBody2, inManifold, ioSettings); + else + OnCollisionBegin(scene, inBody1, inBody2, inManifold, ioSettings); + } + + void JoltContactListener::OnContactPersisted(const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings) + { + OverrideFrictionAndRestitution(inBody1, inBody2, inManifold, ioSettings); + } + + void JoltContactListener::OnContactRemoved(const JPH::SubShapeIDPair& inSubShapePair) + { + Ref scene = ScriptEngine::GetInstance().GetCurrentScene(); + + if (!scene->IsPlaying()) + return; + + JPH::Body* body1 = m_BodyLockInterface->TryGetBody(inSubShapePair.GetBody1ID()); + JPH::Body* body2 = m_BodyLockInterface->TryGetBody(inSubShapePair.GetBody2ID()); + + if (body1 == nullptr || body2 == nullptr) + return; + + if (body1->IsSensor() || body2->IsSensor()) + { + OnTriggerEnd(scene, *body1, *body2); + } + else + { + OnCollisionEnd(scene, *body1, *body2); + } + } + + void JoltContactListener::OnCollisionBegin(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings) + { + m_ContactEventCallback(ContactType::CollisionBegin, inBody1.GetUserData(), inBody2.GetUserData()); + } + + void JoltContactListener::OnCollisionEnd(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2) + { + m_ContactEventCallback(ContactType::CollisionEnd, inBody1.GetUserData(), inBody2.GetUserData()); + } + + void JoltContactListener::OnTriggerBegin(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings) + { + m_ContactEventCallback(ContactType::TriggerBegin, inBody1.GetUserData(), inBody2.GetUserData()); + } + + void JoltContactListener::OnTriggerEnd(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2) + { + m_ContactEventCallback(ContactType::TriggerEnd, inBody1.GetUserData(), inBody2.GetUserData()); + } + + void JoltContactListener::OverrideFrictionAndRestitution(const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings) + { + float friction1, restitution1, friction2, restitution2; + GetFrictionAndRestitution(inBody1, inManifold.mSubShapeID1, friction1, restitution1); + GetFrictionAndRestitution(inBody2, inManifold.mSubShapeID2, friction2, restitution2); + + ioSettings.mCombinedFriction = friction1 * friction2; + ioSettings.mCombinedRestitution = glm::max(restitution1, restitution2); + } + + void JoltContactListener::GetFrictionAndRestitution(const JPH::Body& inBody, const JPH::SubShapeID& inSubShapeID, float& outFriction, float& outRestitution) const + { + const JPH::PhysicsMaterial* material = inBody.GetShape()->GetMaterial(inSubShapeID); + + if (material == JPH::PhysicsMaterial::sDefault) + { + outFriction = inBody.GetFriction(); + outRestitution = inBody.GetRestitution(); + } + else + { + // StarEngine Material + const JoltMaterial* customMaterial = static_cast(material); + outFriction = customMaterial->GetFriction(); + outRestitution = customMaterial->GetRestitution(); + } + } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltContactListener.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltContactListener.h new file mode 100644 index 00000000..9baaf235 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltContactListener.h @@ -0,0 +1,66 @@ +#pragma once + +#include "StarEngine/Physics/PhysicsContactCallback.h" + +#include +#include +#include +#include + +namespace StarEngine { + + class Scene; + + class JoltContactListener : public JPH::ContactListener + { + public: + JoltContactListener(const JPH::BodyLockInterfaceNoLock* bodyLockInterface, const ContactCallbackFn& contactCallback) + : m_BodyLockInterface(bodyLockInterface), m_ContactEventCallback(contactCallback) { } + + /// Called after detecting a collision between a body pair, but before calling OnContactAdded and before adding the contact constraint. + /// If the function returns false, the contact will not be added and any other contacts between this body pair will not be processed. + /// This function will only be called once per PhysicsSystem::Update per body pair and may not be called again the next update + /// if a contact persists and no new contact pairs between sub shapes are found. + /// This is a rather expensive time to reject a contact point since a lot of the collision detection has happened already, make sure you + /// filter out the majority of undesired body pairs through the ObjectLayerPairFilter that is registered on the PhysicsSystem. + /// Note that this callback is called when all bodies are locked, so don't use any locking functions! + /// The order of body 1 and 2 is undefined, but when one of the two bodies is dynamic it will be body 1 + virtual JPH::ValidateResult OnContactValidate(const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::CollideShapeResult& inCollisionResult) { return JPH::ValidateResult::AcceptAllContactsForThisBodyPair; } + + /// Called whenever a new contact point is detected. + /// Note that this callback is called when all bodies are locked, so don't use any locking functions! + /// Body 1 and 2 will be sorted such that body 1 ID < body 2 ID, so body 1 may not be dynamic. + /// Note that only active bodies will report contacts, as soon as a body goes to sleep the contacts between that body and all other + /// bodies will receive an OnContactRemoved callback, if this is the case then Body::IsActive() will return false during the callback. + /// When contacts are added, the constraint solver has not run yet, so the collision impulse is unknown at that point. + /// The velocities of inBody1 and inBody2 are the velocities before the contact has been resolved, so you can use this to + /// estimate the collision impulse to e.g. determine the volume of the impact sound to play. + virtual void OnContactAdded(const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings); + + /// Called whenever a contact is detected that was also detected last update. + /// Note that this callback is called when all bodies are locked, so don't use any locking functions! + /// Body 1 and 2 will be sorted such that body 1 ID < body 2 ID, so body 1 may not be dynamic. + virtual void OnContactPersisted(const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings); + + /// Called whenever a contact was detected last update but is not detected anymore. + /// Note that this callback is called when all bodies are locked, so don't use any locking functions! + /// Note that we're using BodyID's since the bodies may have been removed at the time of callback. + /// Body 1 and 2 will be sorted such that body 1 ID < body 2 ID, so body 1 may not be dynamic. + virtual void OnContactRemoved(const JPH::SubShapeIDPair& inSubShapePair); + + private: + void OnCollisionBegin(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings); + void OnCollisionEnd(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2); + void OnTriggerBegin(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings); + void OnTriggerEnd(Ref scene, const JPH::Body& inBody1, const JPH::Body& inBody2); + + private: + void OverrideFrictionAndRestitution(const JPH::Body& inBody1, const JPH::Body& inBody2, const JPH::ContactManifold& inManifold, JPH::ContactSettings& ioSettings); + void GetFrictionAndRestitution(const JPH::Body& inBody, const JPH::SubShapeID& inSubShapeID, float& outFriction, float& outRestitution) const; + + private: + const JPH::BodyLockInterfaceNoLock* m_BodyLockInterface = nullptr; + ContactCallbackFn m_ContactEventCallback; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCookingFactory.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCookingFactory.cpp new file mode 100644 index 00000000..0819ec57 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCookingFactory.cpp @@ -0,0 +1,367 @@ +#include "sepch.h" +#include "JoltCookingFactory.h" + +#include "JoltBinaryStream.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Math/Math.h" +#include "StarEngine/Physics/PhysicsSystem.h" +#include "StarEngine/Project/Project.h" + +#include +#include + +#include +#include + +namespace StarEngine { + + namespace Utils { + + static std::filesystem::path GetCacheDirectory() + { + return Project::GetCacheDirectory() / "Colliders" / "Jolt"; + } + + static void CreateCacheDirectoryIfNeeded() + { + std::filesystem::path cacheDirectory = GetCacheDirectory(); + if (!std::filesystem::exists(cacheDirectory)) + std::filesystem::create_directories(cacheDirectory); + } + } + + void JoltCookingFactory::Init() + { + } + + void JoltCookingFactory::Shutdown() + { + } + + std::pair JoltCookingFactory::CookMesh(Ref colliderAsset, bool invalidateOld) + { + SE_SCOPE_TIMER("CookingFactory::CookMesh"); + + Utils::CreateCacheDirectoryIfNeeded(); + + AssetHandle colliderHandle = colliderAsset->Handle; + + if (!AssetManager::IsAssetHandleValid(colliderHandle)) + { + SE_CORE_ERROR_TAG("Physics", "Tried to cook mesh collider using an invalid mesh!"); + return { ECookingResult::InvalidMesh, ECookingResult::InvalidMesh }; + } + + Ref mesh = AssetManager::GetAsset(colliderAsset->ColliderMesh); + + if (!mesh) + { + SE_CORE_ERROR_TAG("Physics", "Tried to cook mesh collider using an invalid mesh!"); + return { ECookingResult::InvalidMesh, ECookingResult::InvalidMesh }; + } + + SE_CORE_ASSERT(mesh->GetAssetType() == AssetType::StaticMesh || mesh->GetAssetType() == AssetType::Mesh); + bool isStaticMesh = mesh->GetAssetType() == AssetType::StaticMesh; + + // name-handle.hmc + std::string baseFileName = std::format("{0}-{1}", "Mesh", colliderHandle); + + const bool isPhysicalAsset = !AssetManager::GetMemoryAsset(colliderHandle); + + std::filesystem::path simpleColliderFilePath = Utils::GetCacheDirectory() / std::format("{0}-Simple.hmc", baseFileName); + std::filesystem::path complexColliderFilePath = Utils::GetCacheDirectory() / std::format("{0}-Complex.hmc", baseFileName); + + CachedColliderData colliderData; + ECookingResult simpleMeshResult = ECookingResult::Failure; + ECookingResult complexMeshResult = ECookingResult::Failure; + + if (auto meshSource = AssetManager::GetAsset(isStaticMesh ? mesh.As()->GetMeshSource() : mesh.As()->GetMeshSource()); meshSource) + { + const auto& submeshIndices = isStaticMesh ? mesh.As()->GetSubmeshes() : mesh.As()->GetSubmeshes(); + + // Cook or load the simple collider + if (invalidateOld || !std::filesystem::exists(simpleColliderFilePath)) + { + simpleMeshResult = CookConvexMesh(colliderAsset, meshSource, submeshIndices, colliderData.SimpleColliderData); + + if (simpleMeshResult == ECookingResult::Success && !SerializeMeshCollider(simpleColliderFilePath, colliderData.SimpleColliderData)) + { + SE_CORE_ERROR_TAG("Physics", "Failed to cook simple collider mesh, aborting..."); + simpleMeshResult = ECookingResult::Failure; + } + } + else + { + colliderData.SimpleColliderData = DeserializeMeshCollider(simpleColliderFilePath); + simpleMeshResult = ECookingResult::Success; + } + +#ifndef SE_DIST + if (simpleMeshResult == ECookingResult::Success) + GenerateDebugMesh(colliderAsset, isStaticMesh, submeshIndices, colliderData.SimpleColliderData); +#endif + + // Cook or load the complex collider + if (invalidateOld || !std::filesystem::exists(complexColliderFilePath)) + { + complexMeshResult = CookTriangleMesh(colliderAsset, meshSource, submeshIndices, colliderData.ComplexColliderData); + + if (complexMeshResult == ECookingResult::Success && !SerializeMeshCollider(complexColliderFilePath, colliderData.ComplexColliderData)) + { + SE_CORE_ERROR_TAG("Physics", "Failed to cook complex collider mesh, aborting..."); + complexMeshResult = ECookingResult::Failure; + } + } + else + { + colliderData.ComplexColliderData = DeserializeMeshCollider(complexColliderFilePath); + complexMeshResult = ECookingResult::Success; + } + +#ifndef SE_DIST + if (complexMeshResult == ECookingResult::Success) + GenerateDebugMesh(colliderAsset, isStaticMesh, submeshIndices, colliderData.ComplexColliderData); +#endif + + if (simpleMeshResult == ECookingResult::Success || complexMeshResult == ECookingResult::Success) + { + // Add to cache + auto& meshCache = PhysicsSystem::GetMeshCache(); + if (isPhysicalAsset) + meshCache.m_MeshData[colliderAsset->ColliderMesh][colliderHandle] = colliderData; + else + meshCache.m_MeshData[colliderAsset->ColliderMesh][0] = colliderData; + } + } + + return { simpleMeshResult, complexMeshResult }; + } + + ECookingResult JoltCookingFactory::CookConvexMesh(const Ref& colliderAsset, const Ref& meshSource, const std::vector& submeshIndices, MeshColliderData& outData) + { + ECookingResult cookingResult = ECookingResult::Failure; + + const auto& vertices = meshSource->GetVertices(); + const auto& indices = meshSource->GetIndices(); + const auto& submeshes = meshSource->GetSubmeshes(); + + for (auto submeshIndex : submeshIndices) + { + const auto& submesh = submeshes[submeshIndex]; + + if (submesh.VertexCount < 3) + { + outData.Submeshes.emplace_back(); + continue; + } + + JPH::Array positions; + + for (uint32_t i = submesh.BaseIndex / 3; i < (submesh.BaseIndex / 3) + (submesh.IndexCount / 3); i++) + { + const Index& vertexIndex = indices[i]; + const Vertex& v0 = vertices[vertexIndex.V1]; + positions.push_back(JPH::Vec3(v0.Position.x, v0.Position.y, v0.Position.z)); + + const Vertex& v1 = vertices[vertexIndex.V2]; + positions.push_back(JPH::Vec3(v1.Position.x, v1.Position.y, v1.Position.z)); + + const Vertex& v2 = vertices[vertexIndex.V3]; + positions.push_back(JPH::Vec3(v2.Position.x, v2.Position.y, v2.Position.z)); + } + + JPH::RefConst meshSettings = new JPH::ConvexHullShapeSettings(positions); + JPH::Shape::ShapeResult result = meshSettings->Create(); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to cook convex mesh {}. Error: {}", submesh.MeshName, result.GetError()); + + for (auto& existingSubmesh : outData.Submeshes) + existingSubmesh.ColliderData.Release(); + outData.Submeshes.clear(); + + cookingResult = ECookingResult::Failure; + break; + } + + JPH::RefConst shape = result.Get(); + + JoltBinaryStreamWriter bufferWriter; + shape->SaveBinaryState(bufferWriter); + + SubmeshColliderData& data = outData.Submeshes.emplace_back(); + data.ColliderData = bufferWriter.ToBuffer(); + data.Transform = submesh.Transform * glm::scale(glm::mat4(1.0f), colliderAsset->ColliderScale); + cookingResult = ECookingResult::Success; + } + + outData.Type = MeshColliderType::Convex; + return cookingResult; + } + + ECookingResult JoltCookingFactory::CookTriangleMesh(const Ref& colliderAsset, const Ref& meshSource, const std::vector& submeshIndices, MeshColliderData& outData) + { + ECookingResult cookingResult = ECookingResult::Failure; + + const auto& vertices = meshSource->GetVertices(); + const auto& indices = meshSource->GetIndices(); + const auto& submeshes = meshSource->GetSubmeshes(); + + for (auto submeshIndex : submeshIndices) + { + const auto& submesh = submeshes[submeshIndex]; + + if (submesh.VertexCount < 3) + { + outData.Submeshes.emplace_back(); + continue; + } + + JPH::VertexList vertexList; + JPH::IndexedTriangleList triangleList; + + for (uint32_t vertexIndex = submesh.BaseVertex; vertexIndex < submesh.BaseVertex + submesh.VertexCount; vertexIndex++) + { + const Vertex& v = vertices[vertexIndex]; + vertexList.push_back(JPH::Float3(v.Position.x, v.Position.y, v.Position.z)); + } + + for (uint32_t triangleIndex = submesh.BaseIndex / 3; triangleIndex < (submesh.BaseIndex / 3) + (submesh.IndexCount / 3); triangleIndex++) + { + const Index& i = indices[triangleIndex]; + triangleList.push_back(JPH::IndexedTriangle(i.V1, i.V2, i.V3, 0)); + } + + JPH::RefConst meshSettings = new JPH::MeshShapeSettings(vertexList, triangleList); + + JPH::Shape::ShapeResult result = meshSettings->Create(); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to cook triangle mesh {}. Error: {}", submesh.MeshName, result.GetError()); + + for (auto& existingSubmesh : outData.Submeshes) + existingSubmesh.ColliderData.Release(); + outData.Submeshes.clear(); + + cookingResult = ECookingResult::Failure; + break; + } + + JPH::RefConst shape = result.Get(); + + JoltBinaryStreamWriter bufferWriter; + shape->SaveBinaryState(bufferWriter); + + SubmeshColliderData& data = outData.Submeshes.emplace_back(); + data.ColliderData = bufferWriter.ToBuffer(); + data.Transform = submesh.Transform * glm::scale(glm::mat4(1.0f), colliderAsset->ColliderScale); + cookingResult = ECookingResult::Success; + } + + outData.Type = MeshColliderType::Triangle; + return cookingResult; + } + +#ifndef SE_DIST + void JoltCookingFactory::GenerateDebugMesh(const Ref& colliderAsset, const bool isStaticMesh, const std::vector& submeshIndices, const MeshColliderData& colliderData) + { + std::vector vertices; + std::vector indices; + std::vector submeshes; + for (size_t i = 0; i < colliderData.Submeshes.size(); i++) + { + const SubmeshColliderData& submeshData = colliderData.Submeshes[i]; + + // retrieve the shape by restoring from the binary data + JoltBinaryStreamReader bufferReader(submeshData.ColliderData); + auto shapeResult = JPH::Shape::sRestoreFromBinaryState(bufferReader); + if (!shapeResult.HasError()) + { + auto shape = shapeResult.Get(); + auto jphCom = shape->GetCenterOfMass(); + glm::vec3 com = { jphCom.GetX(), jphCom.GetY(), jphCom.GetZ() }; + JPH::Shape::VisitedShapes ioVisitedShapes; + JPH::Shape::Stats stats = shape->GetStatsRecursive(ioVisitedShapes); + + if (stats.mNumTriangles > 0) + { + Submesh& submesh = submeshes.emplace_back(); + submesh.BaseVertex = static_cast(vertices.size()); + submesh.VertexCount = stats.mNumTriangles * 3; + submesh.BaseIndex = static_cast(indices.size()) * 3; + submesh.IndexCount = stats.mNumTriangles * 3; // Watch out. There are 3 "indexes" per StarEngine::Index + submesh.MaterialIndex = 0; + submesh.Transform = submeshData.Transform; + vertices.reserve(vertices.size() + stats.mNumTriangles * 3); + indices.reserve(indices.size() + stats.mNumTriangles); + + JPH::Shape::GetTrianglesContext ioContext; + JPH::AABox inBox; + JPH::Vec3 inPositionCOM = JPH::Vec3::sZero(); + JPH::Quat inRotation = JPH::Quat::sIdentity(); + JPH::Vec3 inScale = JPH::Vec3::sReplicate(1.0f); + shape->GetTrianglesStart(ioContext, inBox, inPositionCOM, inRotation, inScale); + + JPH::Float3 jphVertices[100 * 3]; + int count = 0; + while ((count = shape->GetTrianglesNext(ioContext, 100, jphVertices)) > 0) + { + uint32_t baseIndex = static_cast(vertices.size()); + for (int i = 0; i < count; ++i) + { + Vertex& v1 = vertices.emplace_back(); + v1.Position = { jphVertices[3 * i].x, jphVertices[3 * i].y, jphVertices[3 * i].z }; + v1.Position += com; + + Vertex& v2 = vertices.emplace_back(); + v2.Position = { jphVertices[3 * i + 1].x, jphVertices[3 * i + 1].y, jphVertices[3 * i + 1].z }; + v2.Position += com; + + Vertex& v3 = vertices.emplace_back(); + v3.Position = { jphVertices[3 * i + 2].x, jphVertices[3 * i + 2].y, jphVertices[3 * i + 2].z }; + v3.Position += com; + + Index& index = indices.emplace_back(); + index.V1 = static_cast(baseIndex + 3 * i); + index.V2 = static_cast(baseIndex + 3 * i + 1); + index.V3 = static_cast(baseIndex + 3 * i + 2); + } + } + } + } + } + if (vertices.size() > 0) + { + Ref meshSource = Ref::Create(vertices, indices, submeshes); + AssetManager::AddMemoryOnlyAsset(meshSource); + if (colliderData.Type == MeshColliderType::Convex) + { + if (isStaticMesh) + { + PhysicsSystem::GetMeshCache().AddSimpleDebugMesh(colliderAsset, Ref::Create(meshSource->Handle, submeshIndices, /*generateColliders=*/false)); + } + else + { + PhysicsSystem::GetMeshCache().AddSimpleDebugMesh(colliderAsset, Ref::Create(meshSource->Handle, submeshIndices, /*generateColliders=*/false)); + } + } + else + { + if (isStaticMesh) + { + PhysicsSystem::GetMeshCache().AddComplexDebugMesh(colliderAsset, Ref::Create(meshSource->Handle, submeshIndices, /*generateColliders=*/false)); + } + else + { + PhysicsSystem::GetMeshCache().AddSimpleDebugMesh(colliderAsset, Ref::Create(meshSource->Handle, submeshIndices, /*generateColliders=*/false)); + } + } + } + } +#endif + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCookingFactory.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCookingFactory.h new file mode 100644 index 00000000..3f079e30 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltCookingFactory.h @@ -0,0 +1,23 @@ +#pragma once + +#include "StarEngine/Physics/MeshCookingFactory.h" + +namespace StarEngine { + + class JoltCookingFactory : public MeshCookingFactory + { + public: + virtual void Init() override; + virtual void Shutdown() override; + + virtual std::pair CookMesh(Ref colliderAsset, bool invalidateOld = false) override; + + private: + static ECookingResult CookConvexMesh(const Ref& colliderAsset, const Ref& meshSource, const std::vector& submeshIndices, MeshColliderData& outData); + static ECookingResult CookTriangleMesh(const Ref& colliderAsset, const Ref& meshSource, const std::vector& submeshIndices, MeshColliderData& outData); +#ifndef SE_DIST + static void GenerateDebugMesh(const Ref& colliderAsset, const bool isStaticMesh, const std::vector& submeshIndices, const MeshColliderData& colliderData); +#endif + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltLayerInterface.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltLayerInterface.cpp new file mode 100644 index 00000000..dac933bc --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltLayerInterface.cpp @@ -0,0 +1,21 @@ +#include "sepch.h" +#include "JoltLayerInterface.h" + +namespace StarEngine { + + JoltLayerInterface::JoltLayerInterface() + { + uint32_t layerCount = PhysicsLayerManager::GetLayerCount(); + m_BroadPhaseLayers.resize(layerCount); + + for (const auto& layer : PhysicsLayerManager::GetLayers()) + m_BroadPhaseLayers[layer.LayerID] = JPH::BroadPhaseLayer(layer.LayerID); + } + + JPH::BroadPhaseLayer JoltLayerInterface::GetBroadPhaseLayer(JPH::ObjectLayer inLayer) const + { + SE_CORE_VERIFY(inLayer < m_BroadPhaseLayers.size()); + return m_BroadPhaseLayers[inLayer]; + } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltLayerInterface.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltLayerInterface.h new file mode 100644 index 00000000..51e2ae93 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltLayerInterface.h @@ -0,0 +1,32 @@ +#pragma once + +#include "StarEngine/Core/Base.h" +#include "StarEngine/Physics/PhysicsLayer.h" + +#include + +namespace StarEngine { + + class JoltLayerInterface : public JPH::BroadPhaseLayerInterface + { + public: + JoltLayerInterface(); + virtual ~JoltLayerInterface() = default; + + virtual uint32_t GetNumBroadPhaseLayers() const override { return static_cast(m_BroadPhaseLayers.size()); } + virtual JPH::BroadPhaseLayer GetBroadPhaseLayer(JPH::ObjectLayer inLayer) const override; + +#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED) + /// Get the user readable name of a broadphase layer (debugging purposes) + virtual const char* GetBroadPhaseLayerName(JPH::BroadPhaseLayer inLayer) const override + { + const auto& layer = PhysicsLayerManager::GetLayer((uint32_t)((uint8_t)inLayer)); + return layer.Name.c_str(); + } +#endif + + private: + std::vector m_BroadPhaseLayers; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltMaterial.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltMaterial.cpp new file mode 100644 index 00000000..f80c95c4 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltMaterial.cpp @@ -0,0 +1,6 @@ +#include "sepch.h" +#include "JoltMaterial.h" + +namespace StarEngine { + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltMaterial.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltMaterial.h new file mode 100644 index 00000000..dc8e98df --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltMaterial.h @@ -0,0 +1,34 @@ +#pragma once + +#include "StarEngine/Scene/Components.h" + +#include + +namespace StarEngine { + + class JoltMaterial : public JPH::PhysicsMaterial + { + public: + JoltMaterial() = default; + JoltMaterial(float friction, float restitution) + : m_Friction(friction), m_Restitution(restitution) + {} + + float GetFriction() const { return m_Friction; } + void SetFriction(float friction) { m_Friction = friction; } + + float GetRestitution() const { return m_Restitution; } + void SetRestitution(float restitution) { m_Restitution = restitution; } + + inline static JoltMaterial* FromColliderMaterial(const ColliderMaterial& colliderMaterial) + { + return new JoltMaterial(colliderMaterial.Friction, colliderMaterial.Restitution); + } + + private: + float m_Friction; + float m_Restitution; + + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltProfiler.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltProfiler.cpp new file mode 100644 index 00000000..3634008f --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltProfiler.cpp @@ -0,0 +1,56 @@ +#include "sepch.h" +#include "StarEngine/Core/Application.h" +#include "StarEngine/Debug/Profiler.h" + +#include +#include + +#include + +namespace JPH { + +#define SE_ENABLE_JOLT_PROFILING 0 + +#if SE_ENABLE_PROFILING && defined(JPH_EXTERNAL_PROFILE) && SE_ENABLE_JOLT_PROFILING + + static std::mutex s_Mutex; + static std::unordered_map s_EventDescriptions; + + ExternalProfileMeasurement::ExternalProfileMeasurement(const char* inName, uint32 inColor /* = 0 */) + { + memset(mUserData, 0, sizeof(mUserData)); + + if (Optick::IsActive(Optick::Mode::DEFAULT) || Optick::IsActive(Optick::Mode::TRACER)) + { + std::thread::id threadID = std::this_thread::get_id(); + std::string threadName = StarEngine::Application::IsMainThread() ? "MainThread" : std::format("JoltThread-{}", threadID); + Optick::ThreadScope threadScope(threadName.c_str()); + + s_Mutex.lock(); + Optick::EventDescription*& eventDescription = s_EventDescriptions[inName]; + s_Mutex.unlock(); + + if (eventDescription == nullptr) + eventDescription = Optick::CreateDescription(inName, __FILE__, __LINE__); + + Optick::EventData* eventData = Optick::Event::Start(*eventDescription); + memcpy(mUserData, &eventData, sizeof(Optick::EventData*)); + } + } + + ExternalProfileMeasurement::~ExternalProfileMeasurement() + { + Optick::EventData** eventData = reinterpret_cast(mUserData); + if (eventData != nullptr && *eventData != nullptr) + Optick::Event::Stop(*(*eventData)); + } + +#elif !defined(JPH_PROFILE_ENABLED) && defined(JPH_EXTERNAL_PROFILE) + + ExternalProfileMeasurement::ExternalProfileMeasurement(const char* inName, uint32 inColor /* = 0 */) {} + ExternalProfileMeasurement::~ExternalProfileMeasurement() {} + +#endif + +} + diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltScene.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltScene.cpp new file mode 100644 index 00000000..23ee5153 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltScene.cpp @@ -0,0 +1,729 @@ +#include "sepch.h" +#include "JoltScene.h" +#include "JoltAPI.h" +#include "JoltUtils.h" +#include "JoltShapes.h" +#include "JoltCaptureManager.h" +#include "JoltCharacterController.h" +#include "JoltMaterial.h" + +#include "StarEngine/Physics/PhysicsSystem.h" +#include "StarEngine/Physics/PhysicsLayer.h" + +#include "StarEngine/Core/Application.h" +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Debug/Profiler.h" + +#include "StarEngine/Core/Events/SceneEvents.h" +#include "StarEngine/Core/Input.h" +#include "StarEngine/Utilities/AnimationUtils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace StarEngine { + + static JoltScene* s_CurrentInstance = nullptr; + + class ObjectVsBroadPhaseLayerFilterImpl : public JPH::ObjectVsBroadPhaseLayerFilter + { + public: + bool ShouldCollide(JPH::ObjectLayer inLayer1, JPH::BroadPhaseLayer inLayer2) const override + { + const auto& layerData1 = PhysicsLayerManager::GetLayer(inLayer1); + const auto& layerData2 = PhysicsLayerManager::GetLayer((uint8_t)inLayer2); + + if (layerData1.LayerID == layerData2.LayerID) + return layerData1.CollidesWithSelf; + + return layerData1.CollidesWith & layerData2.BitValue; + } + }; + + class ObjectLayerPairFilterImpl : public JPH::ObjectLayerPairFilter + { + public: + bool ShouldCollide(JPH::ObjectLayer inObject1, JPH::ObjectLayer inObject2) const override + { + const auto& layerData1 = PhysicsLayerManager::GetLayer(inObject1); + const auto& layerData2 = PhysicsLayerManager::GetLayer(inObject2); + + if (layerData1.LayerID == layerData2.LayerID) + return layerData1.CollidesWithSelf; + + return layerData1.CollidesWith & layerData2.BitValue; + } + }; + + static ObjectLayerPairFilterImpl s_ObjectVsObjectLayerFilter; + static ObjectVsBroadPhaseLayerFilterImpl s_ObjectVsBroadPhaseLayerFilter; + + JoltScene::JoltScene(const Ref& scene) + : PhysicsScene(scene) + { + SE_CORE_VERIFY(s_CurrentInstance == nullptr, "Shouldn't have multiple instances of a physics scene!"); + s_CurrentInstance = this; + + if (PhysicsSystem::GetSettings().CaptureOnPlay) + PhysicsSystem::GetAPI()->GetCaptureManager()->BeginCapture(); + + const auto& settings = PhysicsSystem::GetSettings(); + + m_JoltSystem = std::make_unique(); + m_JoltLayerInterface = std::make_unique(); + + m_JoltSystem->Init(20240, 0, 65536, 20240, *m_JoltLayerInterface, s_ObjectVsBroadPhaseLayerFilter, s_ObjectVsObjectLayerFilter); + m_JoltSystem->SetGravity(JoltUtils::ToJoltVector(settings.Gravity)); + + JPH::PhysicsSettings joltSettings; + joltSettings.mNumPositionSteps = settings.PositionSolverIterations; + joltSettings.mNumVelocitySteps = settings.VelocitySolverIterations; + m_JoltSystem->SetPhysicsSettings(joltSettings); + + m_JoltContactListener = std::make_unique(&m_JoltSystem->GetBodyLockInterfaceNoLock(), [this](ContactType contactType, UUID entityA, UUID entityB) + { + OnContactEvent(contactType, entityA, entityB); + }); + + m_BulkAddBuffer = new JPH::BodyID[s_MaxBulkBodyIDBufferSize]; + m_BulkBufferCount = 0; + + m_OverlapIDBuffer = new JPH::BodyID[m_OverlapIDBufferCount]; + + m_JoltSystem->SetContactListener(m_JoltContactListener.get()); + CreateRigidBodies(); + CreateCharacterControllers(); + } + + JoltScene::~JoltScene() + { + Destroy(); + } + + void JoltScene::Destroy() + { + if (m_BulkAddBuffer == nullptr) + return; + + PhysicsSystem::GetAPI()->GetCaptureManager()->EndCapture(); + + // NOTE(Peter): We don't technically have to this, but explicitly cleaning things up is more consistent, and means we explicitly control + // the order that things are destroyed in + m_BulkBufferCount = 0; + delete[] m_BulkAddBuffer; + m_BulkAddBuffer = nullptr; + + m_CharacterControllers.clear(); + m_RigidBodies.clear(); + m_JoltContactListener.reset(); + m_JoltLayerInterface.reset(); + m_JoltSystem.reset(); + + s_CurrentInstance = nullptr; + } + + + void JoltScene::Simulate(float ts) + { + SE_PROFILE_FUNCTION("JoltScene::Simulate"); + + ProcessBulkStorage(); + + PreSimulate(ts); + + // Note (0x): in line with Jolt sample app, character controllers are updated before physics simulation. + for (auto& [entityID, characterController] : m_CharacterControllers) + { + characterController.As()->Simulate(ts); + } + + if (m_CollisionSteps > 0) + { + SE_PROFILE_SCOPE_DYNAMIC("JoltSystem::Update"); + JoltAPI* api = (JoltAPI*)PhysicsSystem::GetAPI(); + m_JoltSystem->Update(m_FixedTimeStep, m_CollisionSteps, api->GetTempAllocator(), api->GetJobThreadPool()); + } + + { + SE_PROFILE_SCOPE_DYNAMIC("JoltScene::SynchronizeTransform"); + + for (auto& [entityID, characterController] : m_CharacterControllers) + { + auto joltCharacterController = characterController.As(); + Entity entity = m_EntityScene->GetEntityWithUUID(entityID); + auto& transformComponent = entity.GetComponent(); + + glm::quat rotation = transformComponent.GetRotation(); + glm::vec3 scale = transformComponent.Scale; + + transformComponent.Translation = JoltUtils::FromJoltVector(joltCharacterController->m_Controller->GetPosition()); + transformComponent.SetRotation(JoltUtils::FromJoltQuat(joltCharacterController->m_Controller->GetRotation())); + m_EntityScene->ConvertToLocalSpace(entity); + transformComponent.Scale = scale; + + Utils::ReorientAnimation(entity, rotation, transformComponent.GetRotation()); + } + + const auto& bodyLockInterface = m_JoltSystem->GetBodyLockInterface(); + JPH::BodyIDVector activeBodies; + m_JoltSystem->GetActiveBodies(JPH::EBodyType::RigidBody, activeBodies); + JPH::BodyLockMultiWrite activeBodiesLock(bodyLockInterface, activeBodies.data(), static_cast(activeBodies.size())); + for (int32_t i = 0; i < activeBodies.size(); i++) + { + JPH::Body* body = activeBodiesLock.GetBody(i); + + // The position of kinematic rigid bodies is synced _before_ the physics simulation by setting its velocity such that + // the simulation will move it to the game world position. This gives a better collision response than synching the + // position here. + if (body == nullptr || body->IsKinematic()) + continue; + + // Apply air resistance + //body->ApplyBuoyancyImpulse(s_AirPlane, 0.075f, 0.15f, 0.01f, JPH::Vec3::sZero(), m_JoltSystem->GetGravity(), m_FixedTimeStep); + + // Synchronize the transform + Entity entity = m_EntityScene->TryGetEntityWithUUID(body->GetUserData()); + + if (!entity) + continue; + + auto rigidBody = m_RigidBodies.at(entity.GetUUID()); + + auto& transformComponent = entity.GetComponent(); + glm::quat rotation = transformComponent.GetRotation(); + glm::vec3 scale = transformComponent.Scale; + transformComponent.Translation = JoltUtils::FromJoltVector(body->GetPosition()); + + if (!rigidBody->IsAllRotationLocked()) + transformComponent.SetRotation(JoltUtils::FromJoltQuat(body->GetRotation())); + + m_EntityScene->ConvertToLocalSpace(entity); + transformComponent.Scale = scale; + + Utils::ReorientAnimation(entity, rotation, transformComponent.GetRotation()); + } + } + + PostSimulate(); + } + + Ref JoltScene::CreateBody(Entity entity, BodyAddType addType) + { + if (!entity.HasAny()) + { + // This can happen as a result of a user trying to create a RigidBodyComponent from C# script on an entity that does + // not have a collider. + // Tell them what went wrong, rather than silently doing nothing. + SE_CONSOLE_LOG_ERROR("Entity does not have a collider component!"); + return nullptr; + } + + if (auto existingBody = GetBody(entity)) + return existingBody; + + JPH::BodyInterface& bodyInterface = m_JoltSystem->GetBodyInterface(); + Ref rigidBody = Ref::Create(bodyInterface, entity); + + if (rigidBody->m_BodyID.IsInvalid()) + return nullptr; + + switch (addType) + { + case BodyAddType::AddBulk: + /*{ + if (m_BulkBufferCount == s_MaxBulkBodyIDBufferSize) + { + ProcessBulkStorage(); + m_BulkBufferCount = 0; + } + + m_BulkAddBuffer[m_BulkBufferCount++] = rigidBody->GetBodyID(); + break; + }*/ + case BodyAddType::AddImmediate: + { + //SE_CORE_INFO_TAG("Physics", "Adding body immediately."); + bodyInterface.AddBody(rigidBody->m_BodyID, JPH::EActivation::Activate); + break; + } + } + + m_RigidBodies[entity.GetUUID()] = rigidBody; + return rigidBody; + } + + void JoltScene::DestroyBody(Entity entity) + { + auto it = m_RigidBodies.find(entity.GetUUID()); + + if (it == m_RigidBodies.end()) + return; + + it->second.As()->Release(); + m_RigidBodies.erase(it); + } + + void JoltScene::SetBodyType(Entity entity, EBodyType bodyType) + { + auto entityBody = GetBody(entity); + + if (!entityBody) + { + // This can happen if the user does silly things in a C# script. + // For example: + // RigidBodyComponent? rb = entity.CreateComponent(); <-- and suppose this fails + // rb.BodyType = EBodyType::Kinematic; <-- This is user error. However, we don't want it to bring down the entire engine. + SE_CONSOLE_LOG_ERROR("Entity does not have a body component!"); + return; + } + + JPH::BodyLockWrite bodyLock(m_JoltSystem->GetBodyLockInterface(), entityBody.As()->m_BodyID); + SE_CORE_VERIFY(bodyLock.Succeeded()); + JPH::Body& body = bodyLock.GetBody(); + + if (bodyType == EBodyType::Static && body.IsActive()) + m_JoltSystem->GetBodyInterfaceNoLock().DeactivateBody(body.GetID()); + + body.SetMotionType(JoltUtils::ToJoltMotionType(bodyType)); + } + + void JoltScene::OnScenePostStart() + { + ProcessBulkStorage(); + } + + bool JoltScene::CastRay(const RayCastInfo* rayCastInfo, SceneQueryHit& outHit) + { + outHit.Clear(); + + JPH::RayCast ray; + ray.mOrigin = JoltUtils::ToJoltVector(rayCastInfo->Origin); + ray.mDirection = JoltUtils::ToJoltVector(glm::normalize(rayCastInfo->Direction)) * rayCastInfo->MaxDistance; + + JPH::ClosestHitCollisionCollector hitCollector; + JPH::RayCastSettings rayCastSettings; + m_JoltSystem->GetNarrowPhaseQuery().CastRay(JPH::RRayCast(ray), rayCastSettings, hitCollector, {}, {}, JoltRayCastBodyFilter(this, rayCastInfo->ExcludedEntities)); + + for (const auto& [entityID, characterController] : m_CharacterControllers) + { + if (rayCastInfo->ExcludedEntities.find(entityID) != rayCastInfo->ExcludedEntities.end()) + continue; + + auto joltCharacterController = characterController.As(); + auto centerOfMassTransform = joltCharacterController->m_Controller->GetCenterOfMassTransform(); + uint32_t id = (uint64_t(entityID) >> 32) & JPH::BodyID::cMaxBodyIndex; + SE_CORE_VERIFY(id < JPH::BodyID::cMaxBodyIndex, "If you get this notify Peter and tell him to revise this code."); + JPH::BodyID bodyID(id, 0); + JPH::TransformedShape transformedShape(centerOfMassTransform.GetTranslation(), centerOfMassTransform.GetRotation().GetQuaternion(), joltCharacterController->m_Controller->GetShape(), bodyID); + transformedShape.CastRay(JPH::RRayCast(ray), rayCastSettings, hitCollector); + } + + if (!hitCollector.HadHit()) + return false; + + // Check if we hit any of the Character Controllers + for (const auto& [entityID, characterController] : m_CharacterControllers) + { + uint32_t id = (uint64_t(entityID) >> 32) & JPH::BodyID::cMaxBodyIndex; + SE_CORE_VERIFY(id < JPH::BodyID::cMaxBodyIndex, "If you get this notify Peter and tell him to revise this code."); + JPH::BodyID bodyID(id, 0); + + if (hitCollector.mHit.mBodyID != bodyID) + continue; + + auto joltCharacterController = characterController.As(); + + JPH::Vec3 hitPosition = ray.GetPointOnRay(hitCollector.mHit.mFraction); + JPH::RMat44 inverseCOM = joltCharacterController->m_Controller->GetCenterOfMassTransform().Inversed(); + const auto* shape = joltCharacterController->m_Controller->GetShape(); + JPH::Vec3 surfaceNormal = inverseCOM.Multiply3x3Transposed(shape->GetSurfaceNormal(hitCollector.mHit.mSubShapeID2, JPH::Vec3(inverseCOM * hitPosition))).Normalized(); + + outHit.HitEntity = entityID; + outHit.Position = JoltUtils::FromJoltVector(hitPosition); + outHit.Normal = JoltUtils::FromJoltVector(surfaceNormal); + outHit.Distance = glm::distance(rayCastInfo->Origin, outHit.Position); + + auto* controllerShape = reinterpret_cast(shape->GetUserData()); + + if (controllerShape == nullptr) + controllerShape = reinterpret_cast(shape->GetSubShapeUserData(hitCollector.mHit.mSubShapeID2)); + + outHit.HitCollider = controllerShape; + + return true; + } + + JPH::BodyLockRead bodyLock(m_JoltSystem->GetBodyLockInterface(), hitCollector.mHit.mBodyID); + if (bodyLock.Succeeded()) + { + const JPH::Body& body = bodyLock.GetBody(); + + JPH::Vec3 hitPosition = ray.GetPointOnRay(hitCollector.mHit.mFraction); + + outHit.HitEntity = body.GetUserData(); + outHit.Position = JoltUtils::FromJoltVector(hitPosition); + outHit.Normal = JoltUtils::FromJoltVector(body.GetWorldSpaceSurfaceNormal(hitCollector.mHit.mSubShapeID2, hitPosition)); + outHit.Distance = glm::distance(rayCastInfo->Origin, outHit.Position); + outHit.HitCollider = reinterpret_cast(body.GetShape()->GetUserData()); + + return true; + } + + return false; + } + + bool JoltScene::CastShape(const ShapeCastInfo* shapeCastInfo, SceneQueryHit& outHit) + { + JPH::Ref shape = nullptr; + + switch (shapeCastInfo->GetCastType()) + { + case ShapeCastType::Box: + { + const auto* boxCastInfo = reinterpret_cast(shapeCastInfo); + shape = new JPH::BoxShape(JoltUtils::ToJoltVector(boxCastInfo->HalfExtent)); + break; + } + case ShapeCastType::Sphere: + { + const auto* sphereCastInfo = reinterpret_cast(shapeCastInfo); + shape = new JPH::SphereShape(sphereCastInfo->Radius); + break; + } + case ShapeCastType::Capsule: + { + const auto* capsuleCastInfo = reinterpret_cast(shapeCastInfo); + shape = new JPH::CapsuleShape(capsuleCastInfo->HalfHeight, capsuleCastInfo->Radius); + break; + } + default: + SE_CORE_VERIFY(false, "Cannot cast mesh shapes!"); + } + + SE_CORE_VERIFY(shape, "Failed to create shape?"); + + JPH::ShapeCast shapeCast = JPH::ShapeCast::sFromWorldTransform( + shape, + JPH::Vec3(1.0f, 1.0f, 1.0f), + JPH::Mat44::sTranslation(JoltUtils::ToJoltVector(shapeCastInfo->Origin)), + JoltUtils::ToJoltVector(glm::normalize(shapeCastInfo->Direction)) * shapeCastInfo->MaxDistance + ); + + JPH::ShapeCastSettings shapeCastSettings; + JPH::ClosestHitCollisionCollector shapeCastCollector; + m_JoltSystem->GetNarrowPhaseQuery().CastShape(JPH::RShapeCast(shapeCast), shapeCastSettings, JPH::RVec3::sZero(), shapeCastCollector, {}, {}, JoltRayCastBodyFilter(this, shapeCastInfo->ExcludedEntities)); + + for (const auto& [entityID, characterController] : m_CharacterControllers) + { + if (shapeCastInfo->ExcludedEntities.find(entityID) != shapeCastInfo->ExcludedEntities.end()) + continue; + + auto joltCharacterController = characterController.As(); + auto centerOfMassTransform = joltCharacterController->m_Controller->GetCenterOfMassTransform(); + JPH::BodyID bodyID((uint64_t(entityID) >> 32) & 0xFFFFFFFF); + JPH::TransformedShape transformedShape(centerOfMassTransform.GetTranslation(), centerOfMassTransform.GetRotation().GetQuaternion(), joltCharacterController->m_Controller->GetShape(), bodyID); + transformedShape.CastShape(JPH::RShapeCast(shapeCast), shapeCastSettings, JPH::RVec3::sZero(), shapeCastCollector); + } + + if (!shapeCastCollector.HadHit()) + return false; + + // Check if we hit any of the Character Controllers + for (const auto& [entityID, characterController] : m_CharacterControllers) + { + JPH::BodyID bodyID((uint64_t(entityID) >> 32) & 0xFFFFFFFF); + + if (shapeCastCollector.mHit.mBodyID2 != bodyID) + continue; + + auto joltCharacterController = characterController.As(); + + glm::vec3 hitPosition = shapeCastInfo->Origin + shapeCastCollector.mHit.mFraction * shapeCastInfo->Direction; + + JPH::RMat44 inverseCOM = joltCharacterController->m_Controller->GetCenterOfMassTransform().Inversed(); + const auto* shape = joltCharacterController->m_Controller->GetShape(); + JPH::Vec3 surfaceNormal = inverseCOM.Multiply3x3Transposed(shape->GetSurfaceNormal(shapeCastCollector.mHit.mSubShapeID2, JPH::Vec3(inverseCOM * JoltUtils::ToJoltVector(hitPosition)))).Normalized(); + + outHit.HitEntity = entityID; + outHit.Position = hitPosition; + outHit.Normal = JoltUtils::FromJoltVector(surfaceNormal); + outHit.Distance = glm::distance(shapeCastInfo->Origin, hitPosition); + + auto* controllerShape = reinterpret_cast(shape->GetUserData()); + + if (controllerShape == nullptr) + controllerShape = reinterpret_cast(shape->GetSubShapeUserData(shapeCastCollector.mHit.mSubShapeID2)); + + outHit.HitCollider = controllerShape; + + shapeCastCollector.Reset(); + return true; + } + + JPH::BodyLockRead bodyLock(m_JoltSystem->GetBodyLockInterface(), shapeCastCollector.mHit.mBodyID2); + if (bodyLock.Succeeded()) + { + const JPH::Body& body = bodyLock.GetBody(); + + glm::vec3 hitPosition = shapeCastInfo->Origin + shapeCastCollector.mHit.mFraction * shapeCastInfo->Direction; + + outHit.HitEntity = body.GetUserData(); + outHit.Position = hitPosition; + outHit.Normal = JoltUtils::FromJoltVector(body.GetWorldSpaceSurfaceNormal(shapeCastCollector.mHit.mSubShapeID2, JoltUtils::ToJoltVector(hitPosition))); + outHit.Distance = glm::distance(shapeCastInfo->Origin, hitPosition); + outHit.HitCollider = reinterpret_cast(body.GetShape()->GetUserData()); + + return true; + } + + return false; + } + + int32_t JoltScene::OverlapShape(const ShapeOverlapInfo* shapeOverlapInfo, SceneQueryHit** outHits) + { + m_OverlapHitBuffer.clear(); + + JPH::Ref shape = nullptr; + + switch (shapeOverlapInfo->GetCastType()) + { + case ShapeCastType::Box: + { + const auto* boxCastInfo = reinterpret_cast(shapeOverlapInfo); + shape = new JPH::BoxShape(JoltUtils::ToJoltVector(boxCastInfo->HalfExtent)); + break; + } + case ShapeCastType::Sphere: + { + const auto* sphereCastInfo = reinterpret_cast(shapeOverlapInfo); + shape = new JPH::SphereShape(sphereCastInfo->Radius); + break; + } + case ShapeCastType::Capsule: + { + const auto* capsuleCastInfo = reinterpret_cast(shapeOverlapInfo); + shape = new JPH::CapsuleShape(capsuleCastInfo->HalfHeight, capsuleCastInfo->Radius); + break; + } + default: + SE_CORE_VERIFY(false, "Cannot overlap mesh shapes!"); + } + + SE_CORE_VERIFY(shape, "Failed to create shape?"); + + JPH::Mat44 worldTransform = JPH::Mat44::sTranslation(JoltUtils::ToJoltVector(shapeOverlapInfo->Origin)); + JPH::Vec3 shapeScale = JPH::Vec3(1.0f, 1.0f, 1.0f); + + JPH::CollideShapeSettings settings; + JPH::AllHitCollisionCollector collector; + m_JoltSystem->GetNarrowPhaseQuery().CollideShape(shape, shapeScale, worldTransform, settings, JPH::RVec3::sZero(), collector, {}, {}, JoltRayCastBodyFilter(this, shapeOverlapInfo->ExcludedEntities)); + + int numBodies = static_cast(collector.mHits.size()); + + // Resize body id buffer if needed + if (numBodies > m_OverlapIDBufferCount) + { + delete[] m_OverlapIDBuffer; + m_OverlapIDBuffer = new JPH::BodyID[numBodies * 2]; + m_OverlapIDBufferCount = numBodies * 2; + } + + memset(m_OverlapIDBuffer, 0, m_OverlapIDBufferCount * sizeof(JPH::BodyID)); + + // Lock all bodies in collector.mHits + for (size_t i = 0; i < numBodies; i++) + m_OverlapIDBuffer[i] = collector.mHits[i].mBodyID2; + + { + JPH::BodyLockMultiRead bodyLock(m_JoltSystem->GetBodyLockInterface(), m_OverlapIDBuffer, numBodies); + for (int i = 0; i < numBodies; i++) + { + const JPH::Body* body = bodyLock.GetBody(i); + + if (body == nullptr) + continue; + + glm::vec3 hitPosition = JoltUtils::FromJoltVector(collector.mHits[i].mContactPointOn2); + + auto& hitInfo = m_OverlapHitBuffer.emplace_back(); + hitInfo.HitEntity = body->GetUserData(); + hitInfo.Position = hitPosition; + hitInfo.Normal = JoltUtils::FromJoltVector(body->GetWorldSpaceSurfaceNormal(collector.mHits[i].mSubShapeID2, JoltUtils::ToJoltVector(hitPosition))); + hitInfo.Distance = glm::distance(shapeOverlapInfo->Origin, hitPosition); + hitInfo.HitCollider = reinterpret_cast(body->GetShape()->GetUserData()); + } + } + + *outHits = m_OverlapHitBuffer.data(); + return int32_t(m_OverlapHitBuffer.size()); + } + + void JoltScene::Teleport(Entity entity, const glm::vec3& targetPosition, const glm::quat& targetRotation, bool force /*= false*/) + { + auto body = GetBody(entity).As(); + + if (!body) + return; + + m_JoltSystem->GetBodyInterface().SetPositionAndRotationWhenChanged(body->m_BodyID, JoltUtils::ToJoltVector(targetPosition), JoltUtils::ToJoltQuat(targetRotation), JPH::EActivation::Activate); + } + + void JoltScene::MarkKinematic(Ref body) + { + auto it = std::find(m_KinematicBodies.begin(), m_KinematicBodies.end(), body); + + if (it != m_KinematicBodies.end()) + return; + + m_KinematicBodies.push_back(body); + } + + void JoltScene::UnmarkKinematic(Ref body) + { + auto it = std::find(m_KinematicBodies.begin(), m_KinematicBodies.end(), body); + + if (it == m_KinematicBodies.end()) + return; + + m_KinematicBodies.erase(it); + } + + Ref JoltScene::CreateCharacterController(Entity entity) + { + SE_CORE_VERIFY(entity.HasComponent()); + + if (!entity.HasAny() || entity.HasComponent()) + return nullptr; + + Ref characterController = Ref::Create(entity, [this](ContactType contactType, UUID entityA, UUID entityB) + { + OnContactEvent(contactType, entityA, entityB); + }); + m_CharacterControllers[entity.GetUUID()] = characterController; + return characterController; + } + + void JoltScene::DestroyCharacterController(Entity entity) + { + if (auto it = m_CharacterControllers.find(entity.GetUUID()); it != m_CharacterControllers.end()) + m_CharacterControllers.erase(it); + } + + JPH::BodyInterface& JoltScene::GetBodyInterface(bool inShouldLock) + { + SE_CORE_VERIFY(s_CurrentInstance != nullptr); + return inShouldLock ? s_CurrentInstance->m_JoltSystem->GetBodyInterface() : s_CurrentInstance->m_JoltSystem->GetBodyInterfaceNoLock(); + } + + const JPH::BodyLockInterface& JoltScene::GetBodyLockInterface(bool inShouldLock) + { + SE_CORE_VERIFY(s_CurrentInstance != nullptr); + + if (inShouldLock) + return s_CurrentInstance->m_JoltSystem->GetBodyLockInterface(); + else + return s_CurrentInstance->m_JoltSystem->GetBodyLockInterfaceNoLock(); + } + + JPH::PhysicsSystem& JoltScene::GetJoltSystem() { return *s_CurrentInstance->m_JoltSystem.get(); } + + void JoltScene::ProcessBulkStorage() + { + SE_PROFILE_FUNCTION("JoltScene::ProcessBulkStorage"); + + if (m_BulkBufferCount == 0) + return; + + SE_CORE_INFO_TAG("Physics", "Adding {} bodies in bulk!", m_BulkBufferCount); + + // NOTE(Peter): This could be made to happen on a separate background thread, possibly even using Jolt's job system. + // The only consideration we'd have to make is that m_BulkAddBuffer can't change until AddBodiesFinalize has finished. + // This would require us to have CreateBody wait if we're trying to add bodies in bulk. We could possibly + // make this work by having a "back" buffer. + JPH::BodyInterface::AddState addState = m_JoltSystem->GetBodyInterface().AddBodiesPrepare(m_BulkAddBuffer, static_cast(m_BulkBufferCount)); + m_JoltSystem->GetBodyInterface().AddBodiesFinalize(m_BulkAddBuffer, static_cast(m_BulkBufferCount), addState, JPH::EActivation::Activate); + + m_BodiesAddedSinceOptimization += m_BulkBufferCount; + m_BulkBufferCount = 0; + + // NOTE(Peter): Optimizing the broadphase after a lot of bodies have been inserted has the advantage of better structured quad trees. + // The downside is that it can be a relatively heavy operation, especially if there's lots of bodies. + // Optimizing does happen on a background thread, but it does mean JoltSystem::Update has to wait for the new quad trees to be built. + if (m_BodiesAddedSinceOptimization >= s_BroadPhaseOptimizationThreashold) + { + SE_CORE_INFO_TAG("Physics", "Optimizing Broad Phase!"); + m_JoltSystem->OptimizeBroadPhase(); + m_BodiesAddedSinceOptimization = 0; + } + } + + // NOTE(Peter): Technically inefficient since we'd be locking for every call + // But I'm sure it's fine for the most part, this shouldn't be a hot path + void JoltScene::SynchronizeBodyTransform(WeakRef body) + { + SE_PROFILE_FUNCTION("JoltScene::SynchronizeBodyTransform"); + + if (!body.IsValid() || !body->GetEntity()) + return; + + auto joltBody = body.As(); + + JPH::BodyLockRead bodyLock(JoltScene::GetBodyLockInterface(), joltBody->m_BodyID); + SE_CORE_VERIFY(bodyLock.Succeeded()); + const JPH::Body& bodyRef = bodyLock.GetBody(); + + Entity entity = m_EntityScene->GetEntityWithUUID(bodyRef.GetUserData()); + auto& transformComponent = entity.GetComponent(); + glm::vec3 scale = transformComponent.Scale; + transformComponent.Translation = JoltUtils::FromJoltVector(bodyRef.GetPosition()); + transformComponent.SetRotation(JoltUtils::FromJoltQuat(bodyRef.GetRotation())); + + m_EntityScene->ConvertToLocalSpace(entity); + transformComponent.Scale = scale; + } + + /////////// RayCast Filter for determining if a body should be excluded from the ray cast result /////////// + JoltRayCastBodyFilter::JoltRayCastBodyFilter(JoltScene* scene, const ExcludedEntityMap& excludedEntities) + { + m_ExcludedBodies.reserve(excludedEntities.size()); + + for (UUID entityID : excludedEntities) + { + if (Ref entityBody = scene->GetBodyByEntityID(entityID); entityBody) + { + Ref joltBody = entityBody.As(); + m_ExcludedBodies.insert(joltBody->GetBodyID()); + } + else if (Ref entityController = scene->GetCharacterControllerByEntityID(entityID); entityController) + { + Ref joltController = entityController.As(); + m_ExcludedBodies.insert(joltController->GetBodyID()); + } + } + } + + bool JoltRayCastBodyFilter::ShouldCollide(const JPH::BodyID& inBodyID) const + { + return m_ExcludedBodies.find(inBodyID) == m_ExcludedBodies.end(); + } + + bool JoltRayCastBodyFilter::ShouldCollideLocked(const JPH::Body& inBody) const + { + if (inBody.IsSensor()) + { + return false; + } + + return ShouldCollide(inBody.GetID()); + } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltScene.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltScene.h new file mode 100644 index 00000000..a6c9a556 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltScene.h @@ -0,0 +1,97 @@ +#pragma once + +#include "StarEngine/Physics/PhysicsScene.h" + +#include "JoltUtils.h" +#include "JoltLayerInterface.h" +#include "JoltContactListener.h" +#include "JoltBody.h" + +#include +#include + +namespace StarEngine { + + class HZBroadPhaseLayerInterface; + + class JoltScene : public PhysicsScene + { + public: + JoltScene(const Ref& scene); + ~JoltScene(); + + virtual void Destroy() override; + + void Simulate(float ts) override; + + glm::vec3 GetGravity() const override { return JoltUtils::FromJoltVector(m_JoltSystem->GetGravity()); } + void SetGravity(const glm::vec3& gravity) override { m_JoltSystem->SetGravity(JoltUtils::ToJoltVector(gravity)); } + + virtual Ref CreateBody(Entity entity, BodyAddType addType = BodyAddType::AddImmediate) override; + virtual void DestroyBody(Entity entity) override; + + virtual void SetBodyType(Entity entity, EBodyType bodyType) override; + + virtual void OnScenePostStart() override; + + virtual bool CastRay(const RayCastInfo* rayCastInfo, SceneQueryHit& outHit) override; + virtual bool CastShape(const ShapeCastInfo* shapeCastInfo, SceneQueryHit& outHit) override; + virtual int32_t OverlapShape(const ShapeOverlapInfo* shapeOverlapInfo, SceneQueryHit** outHits) override; + + virtual void Teleport(Entity entity, const glm::vec3& targetPosition, const glm::quat& targetRotation, bool force = false) override; + + void MarkKinematic(Ref body); + void UnmarkKinematic(Ref body); + + Ref CreateCharacterController(Entity entity) override; + void DestroyCharacterController(Entity entity) override; + + protected: + void SynchronizeBodyTransform(WeakRef body) override; + + public: + static JPH::BodyInterface& GetBodyInterface(bool inShouldLock = true); + static const JPH::BodyLockInterface& GetBodyLockInterface(bool inShouldLock = true); + static JPH::PhysicsSystem& GetJoltSystem(); + + private: + void ProcessBulkStorage(); + + private: + std::unique_ptr m_JoltSystem; + std::unique_ptr m_JoltLayerInterface; + std::unique_ptr m_JoltContactListener; + + JPH::BodyID* m_BulkAddBuffer = nullptr; + size_t m_BulkBufferCount = 0; + size_t m_BodiesAddedSinceOptimization = 0; + + JPH::BodyID* m_OverlapIDBuffer = nullptr; + size_t m_OverlapIDBufferCount = 20; + + std::vector> m_KinematicBodies; + + inline static JPH::Plane s_AirPlane = JPH::Plane::sFromPointAndNormal(JPH::Vec3(0.0f, 100000.0f, 0.0f), JPH::Vec3(0.0f, 1.0f, 0.0f)); + + private: + static constexpr size_t s_BroadPhaseOptimizationThreashold = 500; + static constexpr size_t s_MaxBulkBodyIDBufferSize = 2500; + + }; + + struct JoltRayCastBodyFilter : public JPH::BodyFilter + { + JoltRayCastBodyFilter(JoltScene* scene, const ExcludedEntityMap& excludedEntities); + virtual ~JoltRayCastBodyFilter() = default; + + /// Filter function. Returns true if we should collide with inBodyID + virtual bool ShouldCollide(const JPH::BodyID& inBodyID) const; + + /// Filter function. Returns true if we should collide with inBody (this is called after the body is locked and makes it possible to filter based on body members) + virtual bool ShouldCollideLocked(const JPH::Body& inBody) const; + + private: + std::unordered_set m_ExcludedBodies; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltShapes.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltShapes.cpp new file mode 100644 index 00000000..fc9519e0 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltShapes.cpp @@ -0,0 +1,320 @@ +#include "sepch.h" +#include "JoltShapes.h" +#include "JoltAPI.h" +#include "JoltBinaryStream.h" + +#include "StarEngine/Physics/PhysicsSystem.h" + +#include "StarEngine/Asset/AssetManager.h" + +#include "StarEngine/Debug/Profiler.h" + +#include +#include + +namespace StarEngine { + + JoltImmutableCompoundShape::JoltImmutableCompoundShape(Entity entity) + : ImmutableCompoundShape(entity) + { + } + + void JoltImmutableCompoundShape::SetMaterial(const ColliderMaterial& material) + { + } + + void JoltImmutableCompoundShape::AddShape(const Ref& shape) + { + Ref scene = Scene::GetScene(m_Entity.GetSceneUUID()); + const TransformComponent rigidBodyTransform = scene->GetWorldSpaceTransform(m_Entity); + const TransformComponent& shapeTransform = shape->GetEntity().Transform(); + + JPH::Shape* nativeShape = static_cast(shape->GetNativeShape()); + + JPH::Vec3 translation = JPH::Vec3::sZero(); + JPH::Quat rotation = JPH::Quat::sIdentity(); + + if (m_Entity != shape->GetEntity()) + { + translation = JoltUtils::ToJoltVector(shapeTransform.Translation * rigidBodyTransform.Scale); + rotation = JoltUtils::ToJoltQuat(shapeTransform.GetRotation()); + } + + m_Settings.AddShape(translation, rotation, nativeShape); + } + + void JoltImmutableCompoundShape::RemoveShape(const Ref& shape) + { + SE_CORE_VERIFY(false, "Cannot remove shape from an ImmutableCompoundShape!"); + } + + void JoltImmutableCompoundShape::Create() + { + const JoltAPI* api = (const JoltAPI*)PhysicsSystem::GetAPI(); + + JPH::ShapeSettings::ShapeResult result = m_Settings.Create(*api->GetTempAllocator()); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to create Immutable Compound Shape. Error: {}", result.GetError()); + return; + } + + m_Shape = JoltUtils::CastJoltRef(result.Get()); + m_Shape->SetUserData(reinterpret_cast(this)); + } + + JoltMutableCompoundShape::JoltMutableCompoundShape(Entity entity) + : MutableCompoundShape(entity) + { + + } + + void JoltMutableCompoundShape::SetMaterial(const ColliderMaterial& material) + { + + } + + void JoltMutableCompoundShape::AddShape(const Ref& shape) + { + Ref scene = Scene::GetScene(m_Entity.GetSceneUUID()); + const TransformComponent rigidBodyTransform = scene->GetWorldSpaceTransform(m_Entity); + const TransformComponent& shapeTransform = shape->GetEntity().Transform(); + + JPH::Shape* nativeShape = static_cast(shape->GetNativeShape()); + + JPH::Vec3 translation = JPH::Vec3::sZero(); + JPH::Quat rotation = JPH::Quat::sIdentity(); + + if (m_Entity != shape->GetEntity()) + { + translation = JoltUtils::ToJoltVector(shapeTransform.Translation * rigidBodyTransform.Scale); + rotation = JoltUtils::ToJoltQuat(shapeTransform.GetRotation()); + } + + m_Settings.AddShape(translation, rotation, nativeShape); + } + + void JoltMutableCompoundShape::RemoveShape(const Ref& shape) + { + } + + void JoltMutableCompoundShape::Create() + { + JPH::ShapeSettings::ShapeResult result = m_Settings.Create(); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to create Mutable Compound Shape. Error: {}", result.GetError()); + return; + } + + m_Shape = JoltUtils::CastJoltRef(result.Get()); + m_Shape->SetUserData(reinterpret_cast(this)); + } + + JoltBoxShape::JoltBoxShape(Entity entity, float totalBodyMass) + : BoxShape(entity) + { + auto scene = Scene::GetScene(entity.GetSceneUUID()); + TransformComponent worldTransform = scene->GetWorldSpaceTransform(entity); + const auto& boxColliderComponent = entity.GetComponent(); + + glm::vec3 halfColliderSize = glm::abs(worldTransform.Scale * boxColliderComponent.HalfSize); + float volume = halfColliderSize.x * 2.0f * halfColliderSize.y * 2.0f * halfColliderSize.z * 2.0f; + + m_Material = JoltMaterial::FromColliderMaterial(boxColliderComponent.Material); + + m_Settings = new JPH::BoxShapeSettings(JoltUtils::ToJoltVector(halfColliderSize), 0.01f); + m_Settings->mDensity = totalBodyMass / volume; + m_Settings->mMaterial = m_Material; + + JPH::RotatedTranslatedShapeSettings offsetShapeSettings(JoltUtils::ToJoltVector(worldTransform.Scale * boxColliderComponent.Offset), JPH::Quat::sIdentity(), m_Settings); + + JPH::ShapeSettings::ShapeResult result = offsetShapeSettings.Create(); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to create BoxShape. Error: {}", result.GetError()); + return; + } + + m_Shape = JoltUtils::CastJoltRef(result.Get()); + m_Shape->SetUserData(reinterpret_cast(this)); + } + + void JoltBoxShape::SetMaterial(const ColliderMaterial& material) + { + m_Material->SetFriction(material.Friction); + m_Material->SetRestitution(material.Restitution); + } + + JoltSphereShape::JoltSphereShape(Entity entity, float totalBodyMass) + : SphereShape(entity) + { + auto scene = Scene::GetScene(entity.GetSceneUUID()); + TransformComponent worldTransform = scene->GetWorldSpaceTransform(entity); + const auto& sphereColliderComponent = entity.GetComponent(); + + float largestComponent = glm::abs(glm::max(worldTransform.Scale.x, glm::max(worldTransform.Scale.y, worldTransform.Scale.z))); + float scaledRadius = largestComponent * sphereColliderComponent.Radius; + float volume = (4.0f / 3.0f) * glm::pi() * (scaledRadius * scaledRadius * scaledRadius); + + m_Material = JoltMaterial::FromColliderMaterial(sphereColliderComponent.Material); + + m_Settings = new JPH::SphereShapeSettings(scaledRadius); + m_Settings->mDensity = totalBodyMass / volume; + m_Settings->mMaterial = m_Material; + + JPH::RotatedTranslatedShapeSettings offsetShapeSettings(JoltUtils::ToJoltVector(worldTransform.Scale * sphereColliderComponent.Offset), JPH::Quat::sIdentity(), m_Settings); + JPH::ShapeSettings::ShapeResult result = offsetShapeSettings.Create(); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to create SphereShape. Error: {}", result.GetError()); + return; + } + + m_Shape = JoltUtils::CastJoltRef(result.Get()); + m_Shape->SetUserData(reinterpret_cast(this)); + } + + void JoltSphereShape::SetMaterial(const ColliderMaterial& material) + { + m_Material->SetFriction(material.Friction); + m_Material->SetRestitution(material.Restitution); + } + + JoltCapsuleShape::JoltCapsuleShape(Entity entity, float totalBodyMass) + : CapsuleShape(entity) + { + auto scene = Scene::GetScene(entity.GetSceneUUID()); + TransformComponent worldTransform = scene->GetWorldSpaceTransform(entity); + const auto& capsuleColliderComponent = entity.GetComponent(); + + float largestRadius = glm::abs(glm::max(worldTransform.Scale.x, worldTransform.Scale.z)); + float scaledRadius = capsuleColliderComponent.Radius * largestRadius; + float scaledHalfHeight = glm::abs(capsuleColliderComponent.HalfHeight * worldTransform.Scale.y); + float volume = glm::pi() * (scaledRadius * scaledRadius) * ((4.0f / 3.0f) * scaledRadius + (scaledHalfHeight * 2.0f)); + + m_Material = JoltMaterial::FromColliderMaterial(capsuleColliderComponent.Material); + + m_Settings = new JPH::CapsuleShapeSettings(scaledHalfHeight, scaledRadius); + m_Settings->mDensity = totalBodyMass / volume; + m_Settings->mMaterial = m_Material; + + JPH::RotatedTranslatedShapeSettings offsetShapeSettings(JoltUtils::ToJoltVector(worldTransform.Scale * capsuleColliderComponent.Offset), JPH::Quat::sIdentity(), m_Settings); + JPH::ShapeSettings::ShapeResult result = offsetShapeSettings.Create(); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to create CapsuleShape. Error: {}", result.GetError()); + return; + } + + m_Shape = JoltUtils::CastJoltRef(result.Get()); + m_Shape->SetUserData(reinterpret_cast(this)); + } + + void JoltCapsuleShape::SetMaterial(const ColliderMaterial& material) + { + m_Material->SetFriction(material.Friction); + m_Material->SetRestitution(material.Restitution); + } + + JoltConvexMeshShape::JoltConvexMeshShape(Entity entity, float totalBodyMass) + : ConvexMeshShape(entity) + { + SE_PROFILE_FUNCTION("JoltConvexMeshShape::JoltConvexMeshShape"); + + auto& component = entity.GetComponent(); + + Ref colliderAsset = AssetManager::GetAsset(component.ColliderAsset); + SE_CORE_VERIFY(colliderAsset); + + auto scene = Scene::GetScene(entity.GetSceneUUID()); + TransformComponent worldTransform = scene->GetWorldSpaceTransform(entity); + + const CachedColliderData& colliderData = PhysicsSystem::GetMeshCache().GetMeshData(colliderAsset); + const auto& meshData = colliderData.SimpleColliderData; + SE_CORE_ASSERT(meshData.Submeshes.size() > component.SubmeshIndex); + + const SubmeshColliderData& submesh = meshData.Submeshes[component.SubmeshIndex]; + glm::vec3 submeshTranslation, submeshScale; + glm::quat submeshRotation; + Math::DecomposeTransform(submesh.Transform, submeshTranslation, submeshRotation, submeshScale); + + JoltBinaryStreamReader binaryReader(submesh.ColliderData); + JPH::Shape::ShapeResult result = JPH::Shape::sRestoreFromBinaryState(binaryReader); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to construct ConvexMesh: {}", result.GetError()); + return; + } + + JPH::Ref convexShape = JoltUtils::CastJoltRef(result.Get()); + convexShape->SetDensity(totalBodyMass / convexShape->GetVolume()); + convexShape->SetMaterial(JoltMaterial::FromColliderMaterial(component.Material)); + + m_Shape = new JPH::ScaledShape(convexShape, JoltUtils::ToJoltVector(submeshScale * worldTransform.Scale)); + m_Shape->SetUserData(reinterpret_cast(this)); + } + + void JoltConvexMeshShape::SetMaterial(const ColliderMaterial& material) + { + } + + // NOTE(Peter): Having materials for triangle shapes is a bit tricky in Jolt... + JoltTriangleMeshShape::JoltTriangleMeshShape(Entity entity) + : TriangleMeshShape(entity) + { + SE_PROFILE_FUNCTION("JoltTriangleMeshShape::JoltTriangleMeshShape"); + + auto& component = entity.GetComponent(); + + Ref colliderAsset = AssetManager::GetAsset(component.ColliderAsset); + SE_CORE_VERIFY(colliderAsset); + + auto scene = Scene::GetScene(entity.GetSceneUUID()); + TransformComponent worldTransform = scene->GetWorldSpaceTransform(entity); + + const CachedColliderData& colliderData = PhysicsSystem::GetMeshCache().GetMeshData(colliderAsset); + const auto& meshData = colliderData.ComplexColliderData; + + JPH::StaticCompoundShapeSettings compoundShapeSettings; + for (const auto& submeshData : meshData.Submeshes) + { + glm::vec3 submeshTranslation, submeshScale; + glm::quat submeshRotation; + Math::DecomposeTransform(submeshData.Transform, submeshTranslation, submeshRotation, submeshScale); + + JoltBinaryStreamReader binaryReader(submeshData.ColliderData); + JPH::Shape::ShapeResult result = JPH::Shape::sRestoreFromBinaryState(binaryReader); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to construct ConvexMesh!"); + return; + } + + compoundShapeSettings.AddShape(JoltUtils::ToJoltVector(submeshTranslation), JoltUtils::ToJoltQuat(submeshRotation), new JPH::ScaledShape(result.Get(), JoltUtils::ToJoltVector(submeshScale * worldTransform.Scale))); + } + + JPH::Shape::ShapeResult result = compoundShapeSettings.Create(); + + if (result.HasError()) + { + SE_CORE_ERROR_TAG("Physics", "Failed to construct ConvexMesh: {}", result.GetError()); + return; + } + + m_Shape = JoltUtils::CastJoltRef(result.Get()); + m_Shape->SetUserData(reinterpret_cast(this)); + } + + void JoltTriangleMeshShape::SetMaterial(const ColliderMaterial& material) + { + } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltShapes.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltShapes.h new file mode 100644 index 00000000..5c691e61 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltShapes.h @@ -0,0 +1,191 @@ +#pragma once + +#include "StarEngine/Physics/PhysicsShapes.h" +#include "JoltUtils.h" +#include "JoltMaterial.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace StarEngine { + + class JoltImmutableCompoundShape : public ImmutableCompoundShape + { + public: + JoltImmutableCompoundShape(Entity entity); + ~JoltImmutableCompoundShape() + { + Destroy(); + } + + virtual void SetMaterial(const ColliderMaterial& material) override; + + virtual void AddShape(const Ref& shape) override; + virtual void RemoveShape(const Ref& shape) override; + virtual void Create() override; + + virtual uint32_t GetNumShapes() const override { return 1; } + virtual void* GetNativeShape(uint32_t index = 0) const override { return m_Shape.GetPtr(); }; + virtual void Destroy() override {} + + private: + JPH::Ref m_Shape = nullptr; + JPH::StaticCompoundShapeSettings m_Settings; + + }; + + class JoltMutableCompoundShape : public MutableCompoundShape + { + public: + JoltMutableCompoundShape(Entity entity); + ~JoltMutableCompoundShape() + { + Destroy(); + } + + virtual void SetMaterial(const ColliderMaterial& material) override; + + virtual void AddShape(const Ref& shape) override; + virtual void RemoveShape(const Ref& shape) override; + virtual void Create() override; + + virtual uint32_t GetNumShapes() const override { return 1; } + virtual void* GetNativeShape(uint32_t index = 0) const override { return m_Shape.GetPtr(); }; + virtual void Destroy() override {} + + private: + JPH::Ref m_Shape = nullptr; + JPH::MutableCompoundShapeSettings m_Settings; + + }; + + class JoltBoxShape : public BoxShape + { + public: + JoltBoxShape(Entity entity, float totalBodyMass); + ~JoltBoxShape() + { + Destroy(); + } + + virtual void SetMaterial(const ColliderMaterial& material) override; + + virtual uint32_t GetNumShapes() const override { return 1; } + virtual void* GetNativeShape(uint32_t index = 0) const override { return m_Shape.GetPtr(); } + + virtual glm::vec3 GetHalfSize() const override { return JoltUtils::FromJoltVector(((const JPH::BoxShape*)m_Shape->GetInnerShape())->GetHalfExtent()); } + + virtual void Destroy() override + { + + } + + private: + JoltMaterial* m_Material = nullptr; + JPH::Ref m_Settings; + JPH::Ref m_Shape = nullptr; + }; + + class JoltSphereShape : public SphereShape + { + public: + JoltSphereShape(Entity entity, float totalBodyMass); + ~JoltSphereShape() + { + Destroy(); + } + + virtual void SetMaterial(const ColliderMaterial& material) override; + + virtual float GetRadius() const override { return((const JPH::CapsuleShape*)m_Shape->GetInnerShape())->GetRadius(); } + + virtual uint32_t GetNumShapes() const override { return 1; } + virtual void* GetNativeShape(uint32_t index = 0) const override { return m_Shape.GetPtr(); } + + virtual void Destroy() override {} + + private: + JoltMaterial* m_Material = nullptr; + JPH::Ref m_Settings; + JPH::Ref m_Shape = nullptr; + }; + + class JoltCapsuleShape : public CapsuleShape + { + public: + JoltCapsuleShape(Entity entity, float totalBodyMass); + ~JoltCapsuleShape() + { + Destroy(); + } + + virtual void SetMaterial(const ColliderMaterial& material) override; + + virtual float GetRadius() const override { return ((const JPH::CapsuleShape*)m_Shape->GetInnerShape())->GetRadius(); } + virtual float GetHeight() const override { return ((const JPH::CapsuleShape*)m_Shape->GetInnerShape())->GetHalfHeightOfCylinder() * 2.0f; } + + virtual uint32_t GetNumShapes() const override { return 1; } + virtual void* GetNativeShape(uint32_t index = 0) const override { return m_Shape.GetPtr(); } + + virtual void Destroy() override {} + + private: + JoltMaterial* m_Material = nullptr; + JPH::Ref m_Settings; + JPH::Ref m_Shape = nullptr; + }; + + class JoltConvexMeshShape : public ConvexMeshShape + { + public: + JoltConvexMeshShape(Entity entity, float totalBodyMass); + ~JoltConvexMeshShape() + { + Destroy(); + } + + virtual void SetMaterial(const ColliderMaterial& material) override; + + virtual uint32_t GetNumShapes() const override { return 1; } + virtual void* GetNativeShape(uint32_t index = 0) const override + { + SE_CORE_VERIFY(index < 1); + return m_Shape.GetPtr(); + } + + virtual void Destroy() override {} + + private: + JPH::Ref m_Shape; + }; + + class JoltTriangleMeshShape : public TriangleMeshShape + { + public: + JoltTriangleMeshShape(Entity entity); + ~JoltTriangleMeshShape() + { + Destroy(); + } + + virtual void SetMaterial(const ColliderMaterial& material) override; + + virtual uint32_t GetNumShapes() const override { return m_Shape->GetNumSubShapes(); } + virtual void* GetNativeShape(uint32_t index = 0) const override + { + SE_CORE_VERIFY(index < 1); + return m_Shape.GetPtr(); + } + + virtual void Destroy() override {} + + private: + JPH::Ref m_Shape; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltUtils.cpp b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltUtils.cpp new file mode 100644 index 00000000..f47b80cc --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltUtils.cpp @@ -0,0 +1,35 @@ +#include "sepch.h" +#include "JoltUtils.h" + +namespace StarEngine::JoltUtils { + + JPH::Vec3 ToJoltVector(const glm::vec3& vector) { return JPH::Vec3(vector.x, vector.y, vector.z); } + JPH::Quat ToJoltQuat(const glm::quat& quat) { return JPH::Quat(quat.x, quat.y, quat.z, quat.w); } + + glm::vec3 FromJoltVector(const JPH::Vec3& vector) { return *(glm::vec3*)&vector; } + glm::quat FromJoltQuat(const JPH::Quat& quat) { return *(glm::quat*)&quat; } + + JPH::EMotionQuality ToJoltMotionQuality(ECollisionDetectionType collisionType) + { + switch (collisionType) + { + case ECollisionDetectionType::Discrete: return JPH::EMotionQuality::Discrete; + case ECollisionDetectionType::Continuous: return JPH::EMotionQuality::LinearCast; + } + + return JPH::EMotionQuality::Discrete; + } + + JPH::EMotionType ToJoltMotionType(EBodyType bodyType) + { + switch (bodyType) + { + case EBodyType::Static: return JPH::EMotionType::Static; + case EBodyType::Dynamic: return JPH::EMotionType::Dynamic; + case EBodyType::Kinematic: return JPH::EMotionType::Kinematic; + } + + return JPH::EMotionType::Static; + } + +} diff --git a/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltUtils.h b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltUtils.h new file mode 100644 index 00000000..d75e3b54 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/JoltPhysics/JoltUtils.h @@ -0,0 +1,39 @@ +#pragma once + +#include "StarEngine/Physics/PhysicsTypes.h" + +#include +#include +#include +#include +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +namespace StarEngine::JoltUtils { + + JPH::Vec3 ToJoltVector(const glm::vec3& vector); + JPH::Quat ToJoltQuat(const glm::quat& quat); + + glm::vec3 FromJoltVector(const JPH::Vec3& vector); + glm::quat FromJoltQuat(const JPH::Quat& quat); + + template + JPH::Ref CastJoltRef(JPH::RefConst ref) + { + return JPH::Ref(static_cast(const_cast(ref.GetPtr()))); + } + + template + JPH::Ref CastJoltRef(const JPH::Ref ref) + { + return JPH::Ref(static_cast(const_cast(ref.GetPtr()))); + } + + JPH::EMotionQuality ToJoltMotionQuality(ECollisionDetectionType collisionType); + JPH::EMotionType ToJoltMotionType(EBodyType bodyType); + +} diff --git a/StarEngine/src/StarEngine/Physics/MeshColliderCache.cpp b/StarEngine/src/StarEngine/Physics/MeshColliderCache.cpp new file mode 100644 index 00000000..b3ca8808 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/MeshColliderCache.cpp @@ -0,0 +1,195 @@ +#include "sepch.h" +#include "MeshColliderCache.h" +#include "PhysicsSystem.h" + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Renderer/MeshFactory.h" + +namespace StarEngine { + + namespace Utils { + + static std::filesystem::path GetCacheDirectory() + { + return Project::GetCacheDirectory() / "Colliders"; + } + + static void CreateCacheDirectoryIfNeeded() + { + std::filesystem::path cacheDirectory = GetCacheDirectory(); + if (!std::filesystem::exists(cacheDirectory)) + std::filesystem::create_directories(cacheDirectory); + } + } + + void MeshColliderCache::Init() + { + m_BoxMesh = MeshFactory::CreateBox(glm::vec3(1)); + m_SphereMesh = MeshFactory::CreateSphere(0.5f); + m_CapsuleMesh = MeshFactory::CreateCapsule(0.5f, 1.0f); + } + + const CachedColliderData& MeshColliderCache::GetMeshData(const Ref& colliderAsset) + { + AssetHandle collisionMesh = colliderAsset->ColliderMesh; + + if (m_MeshData.find(collisionMesh) != m_MeshData.end()) + { + const auto& meshDataMap = m_MeshData.at(collisionMesh); + if (meshDataMap.find(colliderAsset->Handle) != meshDataMap.end()) + return meshDataMap.at(colliderAsset->Handle); + + return meshDataMap.at(0); + } + + // Create/load collision mesh + /* auto[simpleMeshResult, complexMeshResult] = CookingFactory::CookMesh(colliderAsset->Handle); + SE_CORE_ASSERT(m_MeshData.find(collisionMesh) != m_MeshData.end());*/ + const auto& meshDataMap = m_MeshData.at(collisionMesh); + SE_CORE_ASSERT(meshDataMap.find(colliderAsset->Handle) != meshDataMap.end()); + return meshDataMap.at(colliderAsset->Handle); + } + + // Debug meshes get created in CookingFactory::CookMesh + Ref MeshColliderCache::GetDebugMesh(const Ref& colliderAsset) + { + AssetHandle collisionMesh = colliderAsset->ColliderMesh; + auto& map = colliderAsset->CollisionComplexity == ECollisionComplexity::UseComplexAsSimple ? m_DebugComplexMeshes : m_DebugSimpleMeshes; + + if (auto debugMeshes = map.find(collisionMesh); debugMeshes != map.end()) + { + if (auto debugMesh = debugMeshes->second.find(colliderAsset->Handle); debugMesh != debugMeshes->second.end()) + return debugMesh->second; + + if (auto debugMesh = debugMeshes->second.find(0); debugMesh != debugMeshes->second.end()) + return debugMesh->second; + } + + return nullptr; + } + + + Ref MeshColliderCache::GetDebugStaticMesh(const Ref& colliderAsset) + { + AssetHandle collisionMesh = colliderAsset->ColliderMesh; + auto& map = colliderAsset->CollisionComplexity == ECollisionComplexity::UseComplexAsSimple ? m_DebugComplexStaticMeshes : m_DebugSimpleStaticMeshes; + + if (auto debugMeshes = map.find(collisionMesh); debugMeshes != map.end()) + { + if (auto debugMesh = debugMeshes->second.find(colliderAsset->Handle); debugMesh != debugMeshes->second.end()) + return debugMesh->second; + + if (auto debugMesh = debugMeshes->second.find(0); debugMesh != debugMeshes->second.end()) + return debugMesh->second; + } + + return nullptr; + } + + + bool MeshColliderCache::Exists(const Ref& colliderAsset) const + { + AssetHandle collisionMesh = colliderAsset->ColliderMesh; + + if (m_MeshData.find(collisionMesh) == m_MeshData.end()) + return false; + + const auto& meshDataMap = m_MeshData.at(collisionMesh); + if (AssetManager::GetMemoryAsset(colliderAsset->Handle)) + return meshDataMap.find(0) != meshDataMap.end(); + + return meshDataMap.find(colliderAsset->Handle) != meshDataMap.end(); + } + + void MeshColliderCache::Rebuild() + { + /*SE_CORE_INFO_TAG("Physics", "Rebuilding collider cache"); + + if (!FileSystem::DeleteFile(Utils::GetCacheDirectory())) + { + SE_CORE_ERROR_TAG("Physics", "Failed to delete collider cache!"); + return; + } + + Clear(); + + const auto& assetRegistry = Project::GetEditorAssetManager()->GetAssetRegistry(); + const auto& memoryAssets = AssetManager::GetMemoryOnlyAssets(); + + for (const auto& [filepath, metadata] : assetRegistry) + { + if (metadata.Type != AssetType::MeshCollider) + continue; + + auto [simpleMeshResult, complexMeshResult] = CookingFactory::CookMesh(metadata.Handle, true); + + if (simpleMeshResult != CookingResult::Success) + SE_CORE_ERROR_TAG("Physics", "Failed to cook simple collider for '{0}'", metadata.Handle); + + if (complexMeshResult != CookingResult::Success) + SE_CORE_ERROR_TAG("Physics", "Failed to cook complex collider for '{0}'", metadata.Handle); + } + + for (const auto& [handle, asset] : memoryAssets) + { + if (asset->GetAssetType() != AssetType::MeshCollider) + continue; + + auto[simpleMeshResult, complexMeshResult] = CookingFactory::CookMesh(handle, true); + + if (simpleMeshResult != CookingResult::Success) + SE_CORE_ERROR_TAG("Physics", "Failed to cook simple collider for '{0}'", handle); + + if (complexMeshResult != CookingResult::Success) + SE_CORE_ERROR_TAG("Physics", "Failed to cook complex collider for '{0}'", handle); + } + + SE_CORE_INFO_TAG("Physics", "Finished rebuilding collider cache");*/ + } + + void MeshColliderCache::Clear() + { + m_MeshData.clear(); + m_DebugSimpleStaticMeshes.clear(); + m_DebugSimpleMeshes.clear(); + m_DebugComplexStaticMeshes.clear(); + m_DebugComplexMeshes.clear(); + } + + void MeshColliderCache::AddSimpleDebugMesh(const Ref& colliderAsset, const Ref& debugMesh) + { + SE_CORE_ASSERT(debugMesh->GetAssetType() == AssetType::StaticMesh, "Wrong overload of AddSimpleDebugMesh() called. Expected Ref!"); + if (AssetManager::GetMemoryAsset(colliderAsset->Handle)) + m_DebugSimpleStaticMeshes[colliderAsset->ColliderMesh][0] = debugMesh; + else + m_DebugSimpleStaticMeshes[colliderAsset->ColliderMesh][colliderAsset->Handle] = debugMesh; + } + + void MeshColliderCache::AddSimpleDebugMesh(const Ref& colliderAsset, const Ref& debugMesh) + { + SE_CORE_ASSERT(debugMesh->GetAssetType() == AssetType::Mesh, "Wrong overload of AddSimpleDebugMesh() called. Expected Ref!"); + if (AssetManager::GetMemoryAsset(colliderAsset->Handle)) + m_DebugSimpleMeshes[colliderAsset->ColliderMesh][0] = debugMesh; + else + m_DebugSimpleMeshes[colliderAsset->ColliderMesh][colliderAsset->Handle] = debugMesh; + } + + void MeshColliderCache::AddComplexDebugMesh(const Ref& colliderAsset, const Ref& debugMesh) + { + SE_CORE_ASSERT(debugMesh->GetAssetType() == AssetType::StaticMesh, "Wrong overload of AddComplexDebugMesh() called. Expected Ref!"); + if (AssetManager::GetMemoryAsset(colliderAsset->Handle)) + m_DebugComplexStaticMeshes[colliderAsset->ColliderMesh][0] = debugMesh; + else + m_DebugComplexStaticMeshes[colliderAsset->ColliderMesh][colliderAsset->Handle] = debugMesh; + } + + void MeshColliderCache::AddComplexDebugMesh(const Ref& colliderAsset, const Ref& debugMesh) + { + SE_CORE_ASSERT(debugMesh->GetAssetType() == AssetType::Mesh, "Wrong overload of AddComplexDebugMesh() called. Expected Ref!"); + if (AssetManager::GetMemoryAsset(colliderAsset->Handle)) + m_DebugComplexMeshes[colliderAsset->ColliderMesh][0] = debugMesh; + else + m_DebugComplexMeshes[colliderAsset->ColliderMesh][colliderAsset->Handle] = debugMesh; + } + +} diff --git a/StarEngine/src/StarEngine/Physics/MeshColliderCache.h b/StarEngine/src/StarEngine/Physics/MeshColliderCache.h new file mode 100644 index 00000000..db43bcb3 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/MeshColliderCache.h @@ -0,0 +1,48 @@ +#pragma once + +#include "StarEngine/Asset/Asset.h" +#include "MeshCookingFactory.h" + +#include + +namespace StarEngine { + + class MeshColliderCache + { + public: + void Init(); + + const CachedColliderData& GetMeshData(const Ref& colliderAsset); + Ref GetDebugMesh(const Ref& colliderAsset); + Ref GetDebugStaticMesh(const Ref& colliderAsset); + + AssetHandle GetBoxDebugMesh() const { return m_BoxMesh; } + AssetHandle GetSphereDebugMesh() const { return m_SphereMesh; } + AssetHandle GetCapsuleDebugMesh() const { return m_CapsuleMesh; } + + bool Exists(const Ref& colliderAsset) const; + void Rebuild(); + void Clear(); + + private: + void AddSimpleDebugMesh(const Ref& colliderAsset, const Ref& debugMesh); + void AddSimpleDebugMesh(const Ref& colliderAsset, const Ref& debugMesh); + void AddComplexDebugMesh(const Ref& colliderAsset, const Ref& debugMesh); + void AddComplexDebugMesh(const Ref& colliderAsset, const Ref& debugMesh); + + private: + std::unordered_map> m_MeshData; + + // Editor-only + std::unordered_map>> m_DebugSimpleStaticMeshes; + std::unordered_map>> m_DebugSimpleMeshes; + std::unordered_map>> m_DebugComplexStaticMeshes; + std::unordered_map>> m_DebugComplexMeshes; + + AssetHandle m_BoxMesh = 0, m_SphereMesh = 0, m_CapsuleMesh = 0; + + friend class PhysXCookingFactory; + friend class JoltCookingFactory; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/MeshCookingFactory.cpp b/StarEngine/src/StarEngine/Physics/MeshCookingFactory.cpp new file mode 100644 index 00000000..648098ba --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/MeshCookingFactory.cpp @@ -0,0 +1,74 @@ +#include "sepch.h" +#include "MeshCookingFactory.h" +#include "PhysicsAPI.h" +#include "StarEngine/Asset/AssetManager.h" + +#include + +namespace StarEngine { + + std::pair MeshCookingFactory::CookMesh(AssetHandle colliderHandle, bool invalidateOld /*= false*/) + { + return CookMesh(AssetManager::GetAsset(colliderHandle), invalidateOld); + } + + bool MeshCookingFactory::SerializeMeshCollider(const std::filesystem::path& filepath, MeshColliderData& meshData) + { + StarEnginePhysicsMesh hmc; + hmc.Type = meshData.Type; + hmc.SubmeshCount = (uint32_t)meshData.Submeshes.size(); + + std::ofstream stream(filepath, std::ios::binary | std::ios::trunc); + if (!stream) + { + stream.close(); + SE_CORE_ERROR("Failed to write collider to {0}", filepath.string()); + for (auto& submesh : meshData.Submeshes) + submesh.ColliderData.Release(); + meshData.Submeshes.clear(); + return false; + } + + stream.write((char*)&hmc, sizeof(StarEnginePhysicsMesh)); + for (auto& submesh : meshData.Submeshes) + { + stream.write((char*)glm::value_ptr(submesh.Transform), sizeof(submesh.Transform)); + stream.write((char*)&submesh.ColliderData.Size, sizeof(submesh.ColliderData.Size)); + stream.write((char*)submesh.ColliderData.Data, submesh.ColliderData.Size); + } + + return true; + } + + MeshColliderData MeshCookingFactory::DeserializeMeshCollider(const std::filesystem::path& filepath) + { + // Deserialize + Buffer colliderBuffer = FileSystem::ReadBytes(filepath); + StarEnginePhysicsMesh& hmc = *(StarEnginePhysicsMesh*)colliderBuffer.Data; + SE_CORE_VERIFY(strcmp(hmc.Header, StarEnginePhysicsMesh().Header) == 0); + + MeshColliderData meshData; + meshData.Type = hmc.Type; + meshData.Submeshes.resize(hmc.SubmeshCount); + + uint8_t* buffer = colliderBuffer.As() + sizeof(StarEnginePhysicsMesh); + for (uint32_t i = 0; i < hmc.SubmeshCount; i++) + { + SubmeshColliderData& submeshData = meshData.Submeshes[i]; + + // Transform + memcpy(glm::value_ptr(submeshData.Transform), buffer, sizeof(glm::mat4)); + buffer += sizeof(glm::mat4); + + // Data + auto size = submeshData.ColliderData.Size = *(uint64_t*)buffer; + buffer += sizeof(size); + submeshData.ColliderData = Buffer::Copy(buffer, size); + buffer += size; + } + + colliderBuffer.Release(); + return meshData; + } + +} diff --git a/StarEngine/src/StarEngine/Physics/MeshCookingFactory.h b/StarEngine/src/StarEngine/Physics/MeshCookingFactory.h new file mode 100644 index 00000000..95819e31 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/MeshCookingFactory.h @@ -0,0 +1,54 @@ +#pragma once + +#include "StarEngine/Core/Base.h" +#include "StarEngine/Renderer/Mesh.h" +#include "StarEngine/Asset/MeshColliderAsset.h" +#include "PhysicsTypes.h" + +namespace StarEngine { + + enum class MeshColliderType : uint8_t { Triangle = 0, Convex = 1, None = 3 }; + + struct SubmeshColliderData + { + Buffer ColliderData; + glm::mat4 Transform; + }; + + struct MeshColliderData + { + std::vector Submeshes; + MeshColliderType Type; + }; + + struct CachedColliderData + { + MeshColliderData SimpleColliderData; + MeshColliderData ComplexColliderData; + }; + + // .hmc file format + struct StarEnginePhysicsMesh + { + const char Header[8] = "StarEMC"; + MeshColliderType Type; + uint32_t SubmeshCount; + }; + + class MeshCookingFactory : public RefCounted + { + public: + virtual ~MeshCookingFactory() = default; + + virtual void Init() = 0; + virtual void Shutdown() = 0; + + virtual std::pair CookMesh(Ref colliderAsset, bool invalidateOld = false) = 0; + std::pair CookMesh(AssetHandle colliderHandle, bool invalidateOld = false); + + protected: + bool SerializeMeshCollider(const std::filesystem::path& filepath, MeshColliderData& meshData); + MeshColliderData DeserializeMeshCollider(const std::filesystem::path& filepath); + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsAPI.h b/StarEngine/src/StarEngine/Physics/PhysicsAPI.h new file mode 100644 index 00000000..1053b099 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsAPI.h @@ -0,0 +1,36 @@ +#pragma once + +#include "MeshCookingFactory.h" +#include "PhysicsScene.h" +#include "PhysicsBody.h" +#include "PhysicsCaptureManager.h" + +namespace StarEngine { + + enum class PhysicsAPIType + { + Jolt + }; + + class PhysicsAPI + { + public: + virtual ~PhysicsAPI() = default; + + virtual void Init() = 0; + virtual void Shutdown() = 0; + + virtual Ref GetMeshCookingFactory() const = 0; + virtual Ref CreateScene(const Ref& scene) const = 0; + virtual Ref GetCaptureManager() const = 0; + + virtual const std::string& GetLastErrorMessage() const = 0; + + static PhysicsAPIType Current() { return s_CurrentPhysicsAPI; } + static void SetCurrentAPI(PhysicsAPIType api) { s_CurrentPhysicsAPI = api; } + + private: + inline static PhysicsAPIType s_CurrentPhysicsAPI = PhysicsAPIType::Jolt; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsBody.cpp b/StarEngine/src/StarEngine/Physics/PhysicsBody.cpp new file mode 100644 index 00000000..2ba75672 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsBody.cpp @@ -0,0 +1,115 @@ +#include "sepch.h" +#include "PhysicsBody.h" +#include "PhysicsSystem.h" +#include "PhysicsAPI.h" + +#include "StarEngine/Asset/AssetManager.h" + +#include + +#include "StarEngine/Asset/MeshColliderAsset.h" +using namespace magic_enum::bitwise_operators; + +namespace StarEngine { + + void PhysicsBody::SetAxisLock(EActorAxis axis, bool locked, bool forceWake) + { + if (locked) + m_LockedAxes |= axis; + else + m_LockedAxes &= ~axis; + + OnAxisLockUpdated(forceWake); + } + + bool PhysicsBody::IsAxisLocked(EActorAxis axis) const + { + return (m_LockedAxes & axis) != EActorAxis::None; + } + + void PhysicsBody::CreateCollisionShapesForEntity(Entity entity, bool ignoreCompoundShapes) + { + Ref scene = Scene::GetScene(entity.GetSceneUUID()); + + if (entity.HasComponent() && !ignoreCompoundShapes) + { + const auto& compoundColliderComponent = entity.GetComponent(); + + Ref compoundShape = nullptr; + + if (compoundColliderComponent.IsImmutable) + compoundShape = ImmutableCompoundShape::Create(entity); + else + compoundShape = MutableCompoundShape::Create(entity); + + for (auto colliderEntityID : compoundColliderComponent.CompoundedColliderEntities) + { + Entity colliderEntity = scene->TryGetEntityWithUUID(colliderEntityID); + + if (!colliderEntity) + continue; + + CreateCollisionShapesForEntity(colliderEntity, colliderEntity == m_Entity || colliderEntity == entity); + } + + for (const auto& [shapeType, shapeStorage] : m_Shapes) + { + for (const auto& shape : shapeStorage) + compoundShape->AddShape(shape); + } + + compoundShape->Create(); + m_Shapes[compoundShape->GetType()].push_back(compoundShape); + return; + } + + const auto& rigidBodyComponent = m_Entity.GetComponent(); + + if (entity.HasComponent()) + m_Shapes[ShapeType::Box].push_back(BoxShape::Create(entity, rigidBodyComponent.Mass)); + + if (entity.HasComponent()) + m_Shapes[ShapeType::Sphere].push_back(SphereShape::Create(entity, rigidBodyComponent.Mass)); + + if (entity.HasComponent()) + m_Shapes[ShapeType::Capsule].push_back(CapsuleShape::Create(entity, rigidBodyComponent.Mass)); + + if (entity.HasComponent()) + { + const auto& component = entity.GetComponent(); + + auto colliderAsset = AssetManager::GetAsset(component.ColliderAsset); + SE_CORE_VERIFY(colliderAsset); + + auto& meshCache = PhysicsSystem::GetMeshCache(); + + if (!meshCache.Exists(colliderAsset)) + { + auto[simpleColliderResult, complexColliderResult] = PhysicsSystem::GetMeshCookingFactory()->CookMesh(colliderAsset); + + if (simpleColliderResult != ECookingResult::Success && complexColliderResult != ECookingResult::Success) + { + SE_CORE_WARN_TAG("Physics", "Tried to add an Mesh Collider with an invalid collider asset. Please make sure your collider has been cooked."); + return; + } + } + + const CachedColliderData& colliderData = meshCache.GetMeshData(colliderAsset); + + if (colliderAsset->CollisionComplexity == ECollisionComplexity::UseComplexAsSimple && rigidBodyComponent.BodyType == EBodyType::Dynamic) + { + SE_CONSOLE_LOG_ERROR("Entity '{0}' ({1}) has a dynamic RigidBodyComponent along with a Complex MeshColliderComponent! This isn't allowed.", m_Entity.Name(), m_Entity.GetUUID()); + return; + } + + // Create and add simple collider + if (colliderData.SimpleColliderData.Submeshes.size() > 0 && colliderAsset->CollisionComplexity != ECollisionComplexity::UseComplexAsSimple) + m_Shapes[ShapeType::ConvexMesh].push_back(ConvexMeshShape::Create(entity, rigidBodyComponent.Mass)); + + // Create and add complex collider + if (colliderData.ComplexColliderData.Submeshes.size() > 0 && colliderAsset->CollisionComplexity != ECollisionComplexity::UseSimpleAsComplex) + m_Shapes[ShapeType::TriangleMesh].push_back(TriangleMeshShape::Create(entity)); + } + } + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsBody.h b/StarEngine/src/StarEngine/Physics/PhysicsBody.h new file mode 100644 index 00000000..31529932 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsBody.h @@ -0,0 +1,118 @@ +#pragma once + +#include "StarEngine/Scene/Scene.h" +#include "PhysicsShapes.h" + +namespace StarEngine { + + using ShapeArray = std::vector>; + + class PhysicsBody : public RefCounted + { + public: + PhysicsBody(Entity entity) + : m_Entity(entity), m_LockedAxes(EActorAxis::None) + { + const auto& rigidBodyComponent = entity.GetComponent(); + m_LockedAxes = rigidBodyComponent.LockedAxes; + } + + virtual ~PhysicsBody() = default; + + Entity GetEntity() { return m_Entity; } + const Entity& GetEntity() const { return m_Entity; } + + uint32_t GetShapeCount(ShapeType type) const + { + if (auto it = m_Shapes.find(type); it != m_Shapes.end()) + return uint32_t(it->second.size()); + + return 0; + } + + const ShapeArray& GetShapes(ShapeType type) const + { + if (auto it = m_Shapes.find(type); it != m_Shapes.end()) + return it->second; + + return ShapeArray(); + } + + Ref GetShape(ShapeType type, uint32_t index) const + { + if (m_Shapes.find(type) == m_Shapes.end() || index >= uint32_t(m_Shapes.at(type).size())) + return nullptr; + + return m_Shapes.at(type).at(index); + } + + virtual void SetCollisionLayer(uint32_t layerID) = 0; + + virtual bool IsStatic() const = 0; + virtual bool IsDynamic() const = 0; + virtual bool IsKinematic() const = 0; + + // For kinematic bodies only. + virtual void MoveKinematic(const glm::vec3& targetPosition, const glm::quat& targetRotation, float deltaSeconds) = 0; + virtual void Rotate(const glm::vec3& inRotationTimesDeltaTime) = 0; + + // For dynamic bodies only. + virtual void SetGravityEnabled(bool isEnabled) = 0; + virtual void AddForce(const glm::vec3& force, EForceMode forceMode = EForceMode::Force, bool forceWake = true) = 0; + virtual void AddForce(const glm::vec3& force, const glm::vec3& location, EForceMode forceMode = EForceMode::Force, bool forceWake = true) = 0; + virtual void AddTorque(const glm::vec3& torque, bool forceWake = true) = 0; + virtual void AddRadialImpulse(const glm::vec3& origin, float radius, float strength, EFalloffMode falloff, bool velocityChange) = 0; + + virtual void ChangeTriggerState(bool isTrigger) = 0; + virtual bool IsTrigger() const = 0; + + virtual float GetMass() const = 0; + virtual void SetMass(float mass) = 0; + + virtual void SetLinearDrag(float inLinearDrag) = 0; + virtual void SetAngularDrag(float inAngularDrag) = 0; + + virtual glm::vec3 GetLinearVelocity() const = 0; + virtual void SetLinearVelocity(const glm::vec3& inVelocity) = 0; + + virtual glm::vec3 GetAngularVelocity() const = 0; + virtual void SetAngularVelocity(const glm::vec3& inVelocity) = 0; + + virtual float GetMaxLinearVelocity() const = 0; + virtual void SetMaxLinearVelocity(float inMaxVelocity) = 0; + + virtual float GetMaxAngularVelocity() const = 0; + virtual void SetMaxAngularVelocity(float inMaxVelocity) = 0; + + virtual bool IsSleeping() const = 0; + virtual void SetSleepState(bool inSleep) = 0; + + virtual void SetCollisionDetectionMode(ECollisionDetectionType collisionDetectionMode) = 0; + + void SetAxisLock(EActorAxis axis, bool locked, bool forceWake); + bool IsAxisLocked(EActorAxis axis) const; + EActorAxis GetLockedAxes() const { return m_LockedAxes; } + bool IsAllRotationLocked() const { return IsAxisLocked(EActorAxis::RotationX) && IsAxisLocked(EActorAxis::RotationY) && IsAxisLocked(EActorAxis::RotationZ); } + + virtual glm::vec3 GetTranslation() const = 0; + virtual glm::quat GetRotation() const = 0; + + // For static bodies only + virtual void SetTranslation(const glm::vec3& translation) = 0; + virtual void SetRotation(const glm::quat& rotation) = 0; + + private: + virtual void OnAxisLockUpdated(bool forceWake) = 0; + + protected: + void CreateCollisionShapesForEntity(Entity entity, bool ignoreCompoundShapes = false); + + protected: + Entity m_Entity; + + std::unordered_map m_Shapes; + + EActorAxis m_LockedAxes; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsCaptureManager.cpp b/StarEngine/src/StarEngine/Physics/PhysicsCaptureManager.cpp new file mode 100644 index 00000000..dbbfce16 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsCaptureManager.cpp @@ -0,0 +1,52 @@ +#include "sepch.h" +#include "PhysicsCaptureManager.h" + +#include "StarEngine/Physics/PhysicsSystem.h" + +#include "StarEngine/Utilities/FileSystem.h" + +namespace StarEngine { + + void PhysicsCaptureManager::ClearCaptures() + { + m_Captures.clear(); + m_RecentCapture = ""; + } + + void PhysicsCaptureManager::RemoveCapture(const std::filesystem::path& capturePath) + { + m_Captures.erase(std::remove_if(m_Captures.begin(), m_Captures.end(), [&capturePath](const std::filesystem::path& filepath) + { + return filepath == capturePath; + })); + } + + PhysicsCaptureManager::PhysicsCaptureManager() + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: + m_CapturesDirectory = std::filesystem::path("Captures") / "Jolt"; + break; + } + + if (!FileSystem::Exists(m_CapturesDirectory)) + FileSystem::CreateDirectory(m_CapturesDirectory); + + for (auto capturePath : std::filesystem::directory_iterator(m_CapturesDirectory)) + { + std::filesystem::path path = capturePath.path(); + m_Captures.push_back(path); + + if (m_RecentCapture.empty()) + { + m_RecentCapture = path; + continue; + } + + if (FileSystem::IsNewer(path, m_RecentCapture)) + m_RecentCapture = path; + } + } + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsCaptureManager.h b/StarEngine/src/StarEngine/Physics/PhysicsCaptureManager.h new file mode 100644 index 00000000..3223864a --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsCaptureManager.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "StarEngine/Core/Ref.h" + +namespace StarEngine { + + class PhysicsCaptureManager : public RefCounted + { + public: + virtual void BeginCapture() = 0; + virtual void CaptureFrame() = 0; + virtual void EndCapture() = 0; + virtual bool IsCapturing() const = 0; + virtual void OpenCapture(const std::filesystem::path& capturePath) const = 0; + + void OpenRecentCapture() const { OpenCapture(m_RecentCapture); } + void ClearCaptures(); + void RemoveCapture(const std::filesystem::path& capturePath); + const std::vector& GetAllCaptures() const { return m_Captures; } + + protected: + PhysicsCaptureManager(); + + protected: + std::filesystem::path m_CapturesDirectory = ""; + std::filesystem::path m_RecentCapture = ""; + std::vector m_Captures; + + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsContactCallback.h b/StarEngine/src/StarEngine/Physics/PhysicsContactCallback.h new file mode 100644 index 00000000..6aaa0ebb --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsContactCallback.h @@ -0,0 +1,12 @@ +#pragma once + +#include "StarEngine/Scene/Entity.h" + +#include + +namespace StarEngine { + + enum class ContactType : int8_t { None = -1, CollisionBegin, CollisionEnd, TriggerBegin, TriggerEnd }; + using ContactCallbackFn = std::function; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsLayer.cpp b/StarEngine/src/StarEngine/Physics/PhysicsLayer.cpp new file mode 100644 index 00000000..6e06dedd --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsLayer.cpp @@ -0,0 +1,159 @@ +#include "sepch.h" +#include "PhysicsLayer.h" +#include "StarEngine/Utilities/ContainerUtils.h" + +namespace StarEngine { + + uint32_t PhysicsLayerManager::AddLayer(const std::string& name, bool setCollisions) + { + for (const auto& layer : s_Layers) + { + if (layer.Name == name) + return layer.LayerID; + } + + uint32_t layerId = GetNextLayerID(); + PhysicsLayer layer = { layerId, name, static_cast(BIT(layerId)), static_cast(BIT(layerId)) }; + s_Layers.insert(s_Layers.begin() + layerId, layer); + s_LayerNames.insert(s_LayerNames.begin() + layerId, name); + + if (setCollisions) + { + for (const auto& layer2 : s_Layers) + { + SetLayerCollision(layer.LayerID, layer2.LayerID, true); + } + } + + return layer.LayerID; + } + + void PhysicsLayerManager::RemoveLayer(uint32_t layerId) + { + PhysicsLayer& layerInfo = GetLayer(layerId); + + for (auto& otherLayer : s_Layers) + { + if (otherLayer.LayerID == layerId) + continue; + + if (otherLayer.CollidesWith & layerInfo.BitValue) + { + otherLayer.CollidesWith &= ~layerInfo.BitValue; + } + } + + Utils::RemoveIf(s_LayerNames, [&](const std::string& name) { return name == layerInfo.Name; }); + Utils::RemoveIf(s_Layers, [&](const PhysicsLayer& layer) { return layer.LayerID == layerId; }); + } + + void PhysicsLayerManager::UpdateLayerName(uint32_t layerId, const std::string& newName) + { + for (const auto& layerName : s_LayerNames) + { + if (layerName == newName) + return; + } + + PhysicsLayer& layer = GetLayer(layerId); + Utils::RemoveIf(s_LayerNames, [&](const std::string& name) { return name == layer.Name; }); + s_LayerNames.insert(s_LayerNames.begin() + layerId, newName); + layer.Name = newName; + } + + void PhysicsLayerManager::SetLayerCollision(uint32_t layerId, uint32_t otherLayer, bool shouldCollide) + { + if (ShouldCollide(layerId, otherLayer) && shouldCollide) + return; + + PhysicsLayer& layerInfo = GetLayer(layerId); + PhysicsLayer& otherLayerInfo = GetLayer(otherLayer); + + if (shouldCollide) + { + layerInfo.CollidesWith |= otherLayerInfo.BitValue; + otherLayerInfo.CollidesWith |= layerInfo.BitValue; + } + else + { + layerInfo.CollidesWith &= ~otherLayerInfo.BitValue; + otherLayerInfo.CollidesWith &= ~layerInfo.BitValue; + } + } + + std::vector PhysicsLayerManager::GetLayerCollisions(uint32_t layerId) + { + const PhysicsLayer& layer = GetLayer(layerId); + + std::vector layers; + for (const auto& otherLayer : s_Layers) + { + if (otherLayer.LayerID == layerId) + continue; + + if (layer.CollidesWith & otherLayer.BitValue) + layers.push_back(otherLayer); + } + + return layers; + } + + PhysicsLayer& PhysicsLayerManager::GetLayer(uint32_t layerId) + { + return layerId >= s_Layers.size() ? s_NullLayer : s_Layers[layerId]; + } + + PhysicsLayer& PhysicsLayerManager::GetLayer(const std::string& layerName) + { + for (auto& layer : s_Layers) + { + if (layer.Name == layerName) + return layer; + } + + return s_NullLayer; + } + + bool PhysicsLayerManager::ShouldCollide(uint32_t layer1, uint32_t layer2) + { + return GetLayer(layer1).CollidesWith & GetLayer(layer2).BitValue; + } + + bool PhysicsLayerManager::IsLayerValid(uint32_t layerId) + { + const PhysicsLayer& layer = GetLayer(layerId); + return layer.LayerID != s_NullLayer.LayerID && layer.IsValid(); + } + + bool PhysicsLayerManager::IsLayerValid(const std::string& layerName) + { + const PhysicsLayer& layer = GetLayer(layerName); + return layer.LayerID != s_NullLayer.LayerID && layer.IsValid(); + } + + void PhysicsLayerManager::ClearLayers() + { + s_Layers.clear(); + s_LayerNames.clear(); + } + + uint32_t PhysicsLayerManager::GetNextLayerID() + { + int32_t lastId = -1; + + for (const auto& layer : s_Layers) + { + if (lastId != -1 && int32_t(layer.LayerID) != lastId + 1) + return uint32_t(lastId + 1); + + lastId = layer.LayerID; + } + + return (uint32_t)s_Layers.size(); + } + + std::vector PhysicsLayerManager::s_Layers; + std::vector PhysicsLayerManager::s_LayerNames; + PhysicsLayer PhysicsLayerManager::s_NullLayer = { (uint32_t)- 1, "NULL", -1, -1}; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsLayer.h b/StarEngine/src/StarEngine/Physics/PhysicsLayer.h new file mode 100644 index 00000000..e4195bc2 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsLayer.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +namespace StarEngine { + + struct PhysicsLayer + { + uint32_t LayerID; + std::string Name; + int32_t BitValue; + int32_t CollidesWith = 0; + bool CollidesWithSelf = true; + + bool IsValid() const + { + return !Name.empty() && BitValue > 0; + } + }; + + class PhysicsLayerManager + { + public: + static uint32_t AddLayer(const std::string& name, bool setCollisions = true); + static void RemoveLayer(uint32_t layerId); + + static void UpdateLayerName(uint32_t layerId, const std::string& newName); + + static void SetLayerCollision(uint32_t layerId, uint32_t otherLayer, bool shouldCollide); + static std::vector GetLayerCollisions(uint32_t layerId); + + static const std::vector& GetLayers() { return s_Layers; } + static const std::vector& GetLayerNames() { return s_LayerNames; } + + static PhysicsLayer& GetLayer(uint32_t layerId); + static PhysicsLayer& GetLayer(const std::string& layerName); + static uint32_t GetLayerCount() { return uint32_t(s_Layers.size()); } + + static bool ShouldCollide(uint32_t layer1, uint32_t layer2); + static bool IsLayerValid(uint32_t layerId); + static bool IsLayerValid(const std::string& layerName); + + static void ClearLayers(); + + private: + static uint32_t GetNextLayerID(); + + private: + static std::vector s_Layers; + static std::vector s_LayerNames; + static PhysicsLayer s_NullLayer; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsScene.cpp b/StarEngine/src/StarEngine/Physics/PhysicsScene.cpp new file mode 100644 index 00000000..891fc4fd --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsScene.cpp @@ -0,0 +1,353 @@ +#include "sepch.h" +#include "PhysicsScene.h" +#include "PhysicsSystem.h" +#include "StarEngine/Asset/AssetManager.h" + +#include "StarEngine/ImGui/PropertyGrid.h" +#include "StarEngine/Debug/Profiler.h" + +#include "StarEngine/Scripting/ScriptEngine.h" + +#include + +namespace StarEngine { + + PhysicsScene::PhysicsScene(const Ref& scene) + : m_EntityScene(scene) + { + m_FixedTimeStep = PhysicsSystem::GetSettings().FixedTimestep; + + m_OverlapHitBuffer.reserve(s_OverlapHitBufferSize); + } + + PhysicsScene::~PhysicsScene() + { + m_EntityScene = nullptr; + } + + Ref PhysicsScene::GetBodyByEntityID(UUID entityID) const + { + if (auto iter = m_RigidBodies.find(entityID); iter != m_RigidBodies.end()) + return iter->second; + + return nullptr; + } + + Ref PhysicsScene::GetCharacterControllerByEntityID(UUID entityID) const + { + if (auto iter = m_CharacterControllers.find(entityID); iter != m_CharacterControllers.end()) + return iter->second; + + return nullptr; + } + + void PhysicsScene::AddRadialImpulse(const glm::vec3& origin, float radius, float strength, EFalloffMode falloff, bool velocityChange) + { + SphereOverlapInfo sphereOverlapInfo; + sphereOverlapInfo.Origin = origin; + sphereOverlapInfo.Radius = radius; + + SceneQueryHit* hitActors; + int32_t hitCount = OverlapShape(&sphereOverlapInfo, &hitActors); + + if (hitCount == 0) + return; + + for (int32_t i = 0; i < hitCount; i++) + { + auto entityBody = GetBodyByEntityID(hitActors[i].HitEntity); + + if (!entityBody || !entityBody->IsDynamic()) + continue; + + entityBody->AddRadialImpulse(origin, radius, strength, falloff, velocityChange); + } + } + + void PhysicsScene::OnContactEvent(ContactType type, UUID entityA, UUID entityB) + { + size_t index = m_NextContactEventIndex.load(); + + if (index == MaxContactEvents) + { + m_NextContactEventIndex = 0; + index = 0; + } + + auto& contactEvent = m_ContactEvents[index]; + contactEvent.Type = type; + contactEvent.EntityA = entityA; + contactEvent.EntityB = entityB; + + m_NextContactEventIndex++; + } + + static void CallScriptContactCallback(const char* methodName, Entity entityA, Entity entityB) + { + if (!entityA.HasComponent()) + return; + + auto& sc = entityA.GetComponent(); + + const auto& scriptEngine = ScriptEngine::GetInstance(); + + if (!scriptEngine.IsValidScript(sc.ScriptID) || !sc.Instance.IsValid()) + return; + + sc.Instance.Invoke(methodName, uint64_t(entityB.GetUUID())); + } + + void PhysicsScene::SubStepStrategy(float ts) + { + SE_PROFILE_SCOPE_DYNAMIC("PhysicsSystem::SubStepStrategy"); + + if (m_Accumulator > m_FixedTimeStep) + m_Accumulator = 0.0f; + + m_Accumulator += ts; + if (m_Accumulator < m_FixedTimeStep) + { + m_CollisionSteps = 0; + return; + } + + m_CollisionSteps = (uint32_t)(m_Accumulator / m_FixedTimeStep); + m_Accumulator -= (float)m_CollisionSteps * m_FixedTimeStep; + }s + + void PhysicsScene::PreSimulate(float ts) + { + SE_PROFILE_FUNCTION("PhysicsScene::PreSimulate"); + + SubStepStrategy(ts); + + auto view = m_EntityScene->GetAllEntitiesWith(); + + const auto& scriptEngine = ScriptEngine::GetInstance(); + + for (auto enttID : view) + { + auto& scriptComponent = view.get(enttID); + + if (!scriptComponent.Instance.IsValid() || !scriptEngine.IsValidScript(scriptComponent.ScriptID)) + continue; + + scriptComponent.Instance.Invoke("OnPhysicsUpdate", 0.0f); + } + + for (auto& [entityID, characterController] : m_CharacterControllers) + characterController->PreSimulate(ts); + + // Synchronize transforms for (static) bodies that have been moved by gameplay + SynchronizePendingBodyTransforms(); + + // For each kinematic body, call MoveKinematic() so that the kinematic body will end up at the StarEngine Entity's position and rotation. + // Note that kinematic bodies will always move the requisite amount. They are _not_ stopped by collisions (even with immovable objects). + // They will, however, push other (dynamic) bodies out of the way. + for (auto& [entityID, body] : m_RigidBodies) + { + if (body->IsKinematic()) + { + Entity entity = m_EntityScene->GetEntityWithUUID(entityID); + auto tc = m_EntityScene->GetWorldSpaceTransform(entity); + + // moving physics bodies is expensive. make sure its worth it. + glm::vec3 currentBodyTranslation = body->GetTranslation(); + glm::quat currentBodyRotation = body->GetRotation(); + if (glm::any(glm::epsilonNotEqual(currentBodyTranslation, tc.Translation, 0.00001f)) || glm::any(glm::epsilonNotEqual(currentBodyRotation, tc.GetRotation(), 0.00001f))) + { + // Note (0x): Jolt does not awaken sleeping kinematic bodies when they are moved. Wake them up (otherwise the body will not move). + if (body->IsSleeping()) + { + body->SetSleepState(false); + } + + glm::vec3 targetTranslation = tc.Translation; + glm::quat targetRotation = tc.GetRotation(); + if (glm::dot(currentBodyRotation, targetRotation) < 0.0f) + { + targetRotation = -targetRotation; + } + + if (glm::any(glm::epsilonNotEqual(currentBodyRotation, targetRotation, 0.000001f))) + { + glm::vec3 currentBodyEuler = glm::eulerAngles(currentBodyRotation); + glm::vec3 targetBodyEuler = glm::eulerAngles(tc.GetRotation()); + + glm::quat rotation = tc.GetRotation() * glm::conjugate(currentBodyRotation); + glm::vec3 rotationEuler = glm::eulerAngles(rotation); + } + + body->MoveKinematic(tc.Translation, targetRotation, ts); + } + } + } + } + + void PhysicsScene::SynchronizePendingBodyTransforms() + { + // Synchronize bodies that requested it explicitly + for (auto body : m_BodiesScheduledForSync) + { + if (!body.IsValid()) + continue; + + SynchronizeBodyTransform(body); + } + + m_BodiesScheduledForSync.clear(); + } + + void PhysicsScene::PostSimulate() + { + SE_PROFILE_FUNCTION("PhysicsScene::PostSimulate"); + + for (auto& [entityID, characterController] : m_CharacterControllers) + characterController->PostSimulate(); + + PhysicsSystem::GetAPI()->GetCaptureManager()->CaptureFrame(); + + { + size_t eventCount = m_NextContactEventIndex.load(); + for (size_t i = 0; i < eventCount; i++) + { + const auto& contactEvent = m_ContactEvents[i]; + Entity entityA = m_EntityScene->TryGetEntityWithUUID(contactEvent.EntityA); + Entity entityB = m_EntityScene->TryGetEntityWithUUID(contactEvent.EntityB); + + if (!entityA || !entityB) + continue; + + switch (contactEvent.Type) + { + case ContactType::CollisionBegin: + { + CallScriptContactCallback("OnCollisionBeginInternal", entityA, entityB); + CallScriptContactCallback("OnCollisionBeginInternal", entityB, entityA); + break; + } + case ContactType::CollisionEnd: + { + CallScriptContactCallback("OnCollisionEndInternal", entityA, entityB); + CallScriptContactCallback("OnCollisionEndInternal", entityB, entityA); + break; + } + case ContactType::TriggerBegin: + { + CallScriptContactCallback("OnTriggerBeginInternal", entityA, entityB); + CallScriptContactCallback("OnTriggerBeginInternal", entityB, entityA); + break; + } + case ContactType::TriggerEnd: + { + CallScriptContactCallback("OnTriggerEndInternal", entityA, entityB); + CallScriptContactCallback("OnTriggerEndInternal", entityB, entityA); + break; + } + } + + m_NextContactEventIndex--; + } + } + } + + void PhysicsScene::CreateRigidBodies() + { + std::vector compoundedEntities; + + // Create CompoundColliderComponent for entities that have a MeshColliderComponent with a CollisionComplexity of Default + // We do this because we have two mesh shapes generated, one convex and one triangle, the convex shape is used for simulation + // and the triangle shape is used for scene queries (e.g raycasts) + { + auto view = m_EntityScene->GetAllEntitiesWith(); + for (auto enttID : view) + { + Entity entity = { enttID, m_EntityScene.Raw() }; + + if (entity.HasComponent()) + continue; + + const auto& meshColliderComponent = view.get(enttID); + if (meshColliderComponent.CollisionComplexity == ECollisionComplexity::Default) + { + auto& compoundColliderComponent = entity.AddComponent(); + compoundColliderComponent.IncludeStaticChildColliders = false; + compoundColliderComponent.IsImmutable = true; + compoundColliderComponent.CompoundedColliderEntities.push_back(entity.GetUUID()); + } + } + } + + { + auto view = m_EntityScene->GetAllEntitiesWith(); + for (auto enttID : view) + { + Entity entity = { enttID, m_EntityScene.Raw() }; + const auto& compoundColliderComponent = entity.GetComponent(); + for (auto entityID : compoundColliderComponent.CompoundedColliderEntities) + compoundedEntities.push_back(entityID); + } + } + + // Add RigidBodyComponent's for entities that have don't have a RigidBodyComponent but have colliders + { + auto view = m_EntityScene->GetAllEntitiesWith(); + for (auto enttID : view) + { + Entity entity = { enttID, m_EntityScene.Raw() }; + + if (entity.HasComponent()) + continue; + + if (entity.HasComponent()) + continue; + + if (!entity.HasAny()) + continue; + + auto found = std::find_if(compoundedEntities.begin(), compoundedEntities.end(), [&entity](const auto& entityID) + { + return entityID == entity.GetUUID(); + }); + + if (found != compoundedEntities.end() && !entity.HasComponent()) + continue; + + auto& rigidBodyComponent = entity.AddComponent(); + rigidBodyComponent.BodyType = EBodyType::Static; + } + } + + // Create RigidBodies for entities that have an explicit RigidBodyComponent + { + auto view = m_EntityScene->GetAllEntitiesWith(); + + for (auto enttID : view) + { + Entity entity = { enttID, m_EntityScene.Raw() }; + + auto found = std::find_if(compoundedEntities.begin(), compoundedEntities.end(), [&entity](const auto& entityID) + { + return entityID == entity.GetUUID(); + }); + + if (found != compoundedEntities.end() && !entity.HasComponent()) + continue; + + CreateBody(entity, BodyAddType::AddImmediate); + } + } + } + + void PhysicsScene::CreateCharacterControllers() + { + auto view = m_EntityScene->GetAllEntitiesWith(); + + for (auto enttID : view) + { + Entity entity = { enttID, m_EntityScene.Raw() }; + CreateCharacterController(entity); + } + } + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsScene.h b/StarEngine/src/StarEngine/Physics/PhysicsScene.h new file mode 100644 index 00000000..56935eaf --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsScene.h @@ -0,0 +1,109 @@ +#pragma once + +#include "PhysicsSettings.h" +#include "PhysicsBody.h" +#include "CharacterController.h" +#include "PhysicsContactCallback.h" +#include "SceneQueries.h" + +// NOTE(Peter): Allocates roughly 800 bytes once, and then reuses +#define OVERLAP_MAX_COLLIDERS 50 + +#include +#include + +namespace StarEngine { + + //class PhysicsRaycastExcludeEntityFilter; + + enum class BodyAddType { AddBulk, AddImmediate }; + + static constexpr size_t s_OverlapHitBufferSize = 50; + + class PhysicsScene : public RefCounted + { + public: + PhysicsScene(const Ref& scene); + virtual ~PhysicsScene(); + + virtual void Destroy() = 0; + + virtual void Simulate(float ts) = 0; + + virtual glm::vec3 GetGravity() const = 0; + virtual void SetGravity(const glm::vec3& gravity) = 0; + + Ref GetBodyByEntityID(UUID entityID) const; + Ref GetBody(Entity entity) const { return GetBodyByEntityID(entity.GetUUID()); } + virtual Ref CreateBody(Entity entity, BodyAddType addType = BodyAddType::AddImmediate) = 0; + virtual void DestroyBody(Entity entity) = 0; + + virtual void SetBodyType(Entity entity, EBodyType bodyType) = 0; + + Ref GetCharacterControllerByEntityID(UUID entityID) const; + Ref GetCharacterController(Entity entity) const { return GetCharacterControllerByEntityID(entity.GetUUID()); } + virtual Ref CreateCharacterController(Entity entity) = 0; + virtual void DestroyCharacterController(Entity entity) = 0; + + const Ref& GetEntityScene() const { return m_EntityScene; } + Ref GetEntityScene() { return m_EntityScene; } + + virtual void OnEvent(Event& event) {} + virtual void OnScenePostStart() {} + + //// Geometry Queries //// + virtual bool CastRay(const RayCastInfo* rayCastInfo, SceneQueryHit& outHit) = 0; + virtual bool CastShape(const ShapeCastInfo* shapeCastInfo, SceneQueryHit& outHit) = 0; + virtual int32_t OverlapShape(const ShapeOverlapInfo* shapeOverlapInfo, SceneQueryHit** outHit) = 0; + + //// Radial Impulse //// + void AddRadialImpulse(const glm::vec3& origin, float radius, float strength, EFalloffMode falloff, bool velocityChange); + + virtual void Teleport(Entity entity, const glm::vec3& targetPosition, const glm::quat& targetRotation, bool force = false) = 0; + + void MarkForSynchronization(Ref body) + { + m_BodiesScheduledForSync.push_back(body); + } + + protected: + void PreSimulate(float ts); + void PostSimulate(); + + void CreateRigidBodies(); + void CreateCharacterControllers(); + void SynchronizePendingBodyTransforms(); + + void OnContactEvent(ContactType type, UUID entityA, UUID entityB); + + virtual void SynchronizeBodyTransform(WeakRef body) = 0; + + private: + void SubStepStrategy(float ts); + + protected: + Ref m_EntityScene; + std::unordered_map> m_RigidBodies; + std::unordered_map> m_CharacterControllers; + + std::vector> m_BodiesScheduledForSync; + + std::vector m_OverlapHitBuffer; + + float m_FixedTimeStep = 1.0f / 60.0f; + float m_Accumulator = 0.0f; + uint32_t m_CollisionSteps = 1; + + private: + static constexpr size_t MaxContactEvents = 10000; + struct ContactEvent { ContactType Type = ContactType::None; UUID EntityA; UUID EntityB; }; + + std::mutex m_ContactEventsMutex; + ContactEvent m_ContactEvents[MaxContactEvents]; + std::atomic m_NextContactEventIndex = 0; + + private: + friend class Scene; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsSettings.h b/StarEngine/src/StarEngine/Physics/PhysicsSettings.h new file mode 100644 index 00000000..2a9d5d38 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsSettings.h @@ -0,0 +1,28 @@ +#pragma once + +#include "PhysicsTypes.h" + +#include + +namespace StarEngine { + + enum class PhysicsDebugType + { + DebugToFile = 0, + LiveDebug + }; + + struct PhysicsSettings + { + float FixedTimestep = 1.0f / 60.0f; + glm::vec3 Gravity = { 0.0f, -9.81f, 0.0f }; + uint32_t PositionSolverIterations = 2; + uint32_t VelocitySolverIterations = 10; + + uint32_t MaxBodies = 5700; + + bool CaptureOnPlay = true; + PhysicsDebugType CaptureMethod = PhysicsDebugType::DebugToFile; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsShapes.cpp b/StarEngine/src/StarEngine/Physics/PhysicsShapes.cpp new file mode 100644 index 00000000..1c65ffd0 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsShapes.cpp @@ -0,0 +1,86 @@ +#include "sepch.h" +#include "PhysicsShapes.h" +#include "PhysicsAPI.h" + +#include "StarEngine/Physics/JoltPhysics/JoltShapes.h" + +namespace StarEngine { + + Ref ImmutableCompoundShape::Create(Entity entity) + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return Ref::Create(entity); + } + + SE_CORE_VERIFY(false); + return nullptr; + } + + Ref MutableCompoundShape::Create(Entity entity) + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return Ref::Create(entity); + } + + SE_CORE_VERIFY(false); + return nullptr; + } + + Ref BoxShape::Create(Entity entity, float totalBodyMass) + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return Ref::Create(entity, totalBodyMass); + } + + SE_CORE_VERIFY(false); + return nullptr; + } + + Ref SphereShape::Create(Entity entity, float totalBodyMass) + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return Ref::Create(entity, totalBodyMass); + } + + SE_CORE_VERIFY(false); + return nullptr; + } + + Ref CapsuleShape::Create(Entity entity, float totalBodyMass) + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return Ref::Create(entity, totalBodyMass); + } + + SE_CORE_VERIFY(false); + return nullptr; + } + + Ref ConvexMeshShape::Create(Entity entity, float totalBodyMass) + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return Ref::Create(entity, totalBodyMass); + } + + SE_CORE_VERIFY(false); + return nullptr; + } + + Ref TriangleMeshShape::Create(Entity entity) + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return Ref::Create(entity); + } + + SE_CORE_VERIFY(false); + return nullptr; + } + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsShapes.h b/StarEngine/src/StarEngine/Physics/PhysicsShapes.h new file mode 100644 index 00000000..4dbce517 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsShapes.h @@ -0,0 +1,168 @@ +#pragma once + +#include "StarEngine/Scene/Entity.h" + +namespace StarEngine { + + enum class ShapeType { Box, Sphere, Capsule, ConvexMesh, TriangleMesh, CompoundShape, MutableCompoundShape, LAST }; + + namespace ShapeUtils { + constexpr size_t MaxShapeTypes = (size_t)ShapeType::LAST; + + inline const char* ShapeTypeToString(ShapeType type) + { + switch (type) + { + case ShapeType::CompoundShape: return "CompoundShape"; + case ShapeType::MutableCompoundShape: return "MutableCompoundShape"; + case ShapeType::Box: return "Box"; + case ShapeType::Sphere: return "Sphere"; + case ShapeType::Capsule: return "Capsule"; + case ShapeType::ConvexMesh: return "ConvexMesh"; + case ShapeType::TriangleMesh: return "TriangleMesh"; + } + + SE_CORE_VERIFY(false); + return ""; + } + + } + + class PhysicsShape : public RefCounted + { + public: + virtual ~PhysicsShape() = default; + + virtual uint32_t GetNumShapes() const = 0; + virtual void* GetNativeShape(uint32_t index = 0) const = 0; + + virtual void SetMaterial(const ColliderMaterial& material) = 0; + + virtual void Destroy() = 0; + + Entity GetEntity() const { return m_Entity; } + ShapeType GetType() const { return m_Type; } + + protected: + PhysicsShape(ShapeType type, Entity entity) + : m_Type(type), m_Entity(entity) {} + + protected: + Entity m_Entity; + + private: + ShapeType m_Type; + }; + + class CompoundShape : public PhysicsShape + { + public: + CompoundShape(Entity entity, const bool isImmutable) + : PhysicsShape(isImmutable ? ShapeType::CompoundShape : ShapeType::MutableCompoundShape, entity) + { + } + virtual ~CompoundShape() = default; + + virtual void AddShape(const Ref& shape) = 0; + virtual void RemoveShape(const Ref& shape) = 0; + virtual void Create() = 0; + }; + + class ImmutableCompoundShape : public CompoundShape + { + public: + ImmutableCompoundShape(Entity entity) + : CompoundShape(entity, true) {} + + virtual ~ImmutableCompoundShape() = default; + + public: + static Ref Create(Entity entity); + }; + + class MutableCompoundShape : public CompoundShape + { + public: + MutableCompoundShape(Entity entity) + : CompoundShape(entity, false) + { + } + + virtual ~MutableCompoundShape() = default; + + public: + static Ref Create(Entity entity); + }; + + class BoxShape : public PhysicsShape + { + public: + BoxShape(Entity entity) + : PhysicsShape(ShapeType::Box, entity) {} + virtual ~BoxShape() = default; + + virtual glm::vec3 GetHalfSize() const = 0; + + public: + static Ref Create(Entity entity, float totalBodyMass); + }; + + class SphereShape : public PhysicsShape + { + public: + SphereShape(Entity entity) + : PhysicsShape(ShapeType::Sphere, entity) {} + + virtual ~SphereShape() = default; + + virtual float GetRadius() const = 0; + + public: + static Ref Create(Entity entity, float totalBodyMass); + }; + + class CapsuleShape : public PhysicsShape + { + public: + CapsuleShape(Entity entity) + : PhysicsShape(ShapeType::Capsule, entity) + { + } + + virtual ~CapsuleShape() = default; + + virtual float GetRadius() const = 0; + + virtual float GetHeight() const = 0; + + public: + static Ref Create(Entity entity, float totalBodyMass); + }; + + class ConvexMeshShape : public PhysicsShape + { + public: + ConvexMeshShape(Entity entity) + : PhysicsShape(ShapeType::ConvexMesh, entity) {} + + virtual ~ConvexMeshShape() = default; + + public: + static Ref Create(Entity entity, float totalBodyMass); + }; + + class TriangleMeshShape : public PhysicsShape + { + public: + TriangleMeshShape(Entity entity) + : PhysicsShape(ShapeType::TriangleMesh, entity) + { + } + + virtual ~TriangleMeshShape() = default; + + public: + static Ref Create(Entity entity); + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsSystem.cpp b/StarEngine/src/StarEngine/Physics/PhysicsSystem.cpp new file mode 100644 index 00000000..5fab5f83 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsSystem.cpp @@ -0,0 +1,95 @@ +#include "sepch.h" +#include "PhysicsSystem.h" + +#include "JoltPhysics/JoltAPI.h" + +#include "StarEngine/Core/Application.h" +#include "StarEngine/Asset/AssetManager.h" + +namespace StarEngine { + + + static PhysicsAPI* InitPhysicsAPI() + { + switch (PhysicsAPI::Current()) + { + case PhysicsAPIType::Jolt: return snew JoltAPI(); + } + + SE_CORE_VERIFY(false, "Unknown PhysicsAPI"); + return nullptr; + } + + void PhysicsSystem::Init() + { + s_PhysicsAPI = InitPhysicsAPI(); + s_PhysicsAPI->Init(); + + s_PhysicsMeshCache.Init(); + + Application::Get().AddEventCallback(OnEvent); + } + + void PhysicsSystem::Shutdown() + { + s_PhysicsMeshCache.Clear(); + s_PhysicsAPI->Shutdown(); + sdelete s_PhysicsAPI; + } + + const std::string& PhysicsSystem::GetLastErrorMessage() { return s_PhysicsAPI->GetLastErrorMessage(); } + + Ref PhysicsSystem::GetOrCreateColliderAsset(Entity entity, MeshColliderComponent& component) + { + Ref colliderAsset = AssetManager::GetAsset(component.ColliderAsset); + + if (colliderAsset) + return colliderAsset; + + if (entity.HasComponent()) + { + auto& mc = entity.GetComponent(); + component.ColliderAsset = AssetManager::AddMemoryOnlyAsset(Ref::Create(mc.Mesh)); + component.SubmeshIndex = mc.SubmeshIndex; + } + else if (entity.HasComponent()) + { + component.ColliderAsset = AssetManager::AddMemoryOnlyAsset(Ref::Create(entity.GetComponent().StaticMesh)); + } + + colliderAsset = AssetManager::GetAsset(component.ColliderAsset); + + if (colliderAsset && !PhysicsSystem::GetMeshCache().Exists(colliderAsset)) + { + s_PhysicsAPI->GetMeshCookingFactory()->CookMesh(component.ColliderAsset); + } + + return colliderAsset; + } + + Ref PhysicsSystem::CreatePhysicsScene(const Ref& scene) { return s_PhysicsAPI->CreateScene(scene); } + + void PhysicsSystem::OnEvent(Event& event) + { + /*EventDispatcher dispatcher(event); + +#ifdef SE_DEBUG + dispatcher.Dispatch([](ScenePreStartEvent& e) + { + if (s_PhysicsSettings.DebugOnPlay && !PhysXDebugger::IsDebugging()) + PhysXDebugger::StartDebugging((Project::GetActive()->GetProjectDirectory() / "PhysXDebugInfo").string(), s_PhysicsSettings.DebugType == PhysicsDebugType::LiveDebug); + return false; + }); + + dispatcher.Dispatch([](ScenePreStopEvent& e) + { + if (s_PhysicsSettings.DebugOnPlay) + PhysXDebugger::StopDebugging(); + return false; + }); +#endif*/ + } + + PhysicsAPI* PhysicsSystem::s_PhysicsAPI = nullptr; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsSystem.h b/StarEngine/src/StarEngine/Physics/PhysicsSystem.h new file mode 100644 index 00000000..7dc9320a --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsSystem.h @@ -0,0 +1,39 @@ +#pragma once + +#include "PhysicsSettings.h" +#include "MeshColliderCache.h" +#include "PhysicsAPI.h" + +#include "StarEngine/Core/Events/SceneEvents.h" + +namespace StarEngine { + + class PhysicsSystem + { + public: + static void Init(); + static void Shutdown(); + + static PhysicsSettings& GetSettings() { return s_PhysicsSettings; } + static MeshColliderCache& GetMeshCache() { return s_PhysicsMeshCache; } + static Ref GetMeshCookingFactory() { return s_PhysicsAPI->GetMeshCookingFactory(); } + static const std::string& GetLastErrorMessage(); + + static Ref GetOrCreateColliderAsset(Entity entity, MeshColliderComponent& component); + + static Ref CreatePhysicsScene(const Ref& scene); + + static PhysicsAPI* GetAPI() { return s_PhysicsAPI; } + private: + static void OnEvent(Event& event); + + private: + static PhysicsAPI* s_PhysicsAPI; + inline static PhysicsSettings s_PhysicsSettings; + inline static MeshColliderCache s_PhysicsMeshCache; + + private: + friend class JoltScene; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics/PhysicsTypes.h b/StarEngine/src/StarEngine/Physics/PhysicsTypes.h new file mode 100644 index 00000000..08de961d --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/PhysicsTypes.h @@ -0,0 +1,52 @@ +#pragma once +#include + +#include "StarEngine/Core/Base.h" + +namespace StarEngine { + + enum class ECookingResult + { + Success, + ZeroAreaTestFailed, + PolygonLimitReached, + LargeTriangle, + InvalidMesh, + Failure, + None + }; + + enum class EForceMode : uint8_t + { + Force = 0, + Impulse, + VelocityChange, + Acceleration + }; + + enum class EActorAxis : uint32_t + { + None = 0, + TranslationX = BIT(0), TranslationY = BIT(1), TranslationZ = BIT(2), Translation = TranslationX | TranslationY | TranslationZ, + RotationX = BIT(3), RotationY = BIT(4), RotationZ = BIT(5), Rotation = RotationX | RotationY | RotationZ + }; + + enum class ECollisionDetectionType : uint32_t + { + Discrete, + Continuous + }; + + enum class ECollisionFlags : uint8_t + { + None, + Sides = BIT(0), + Above = BIT(1), + Below = BIT(2), + }; + + enum class EBodyType { Static, Dynamic, Kinematic }; + + enum class EFalloffMode { Constant, Linear }; + +} diff --git a/StarEngine/src/StarEngine/Physics/SceneQueries.h b/StarEngine/src/StarEngine/Physics/SceneQueries.h new file mode 100644 index 00000000..5768a7fb --- /dev/null +++ b/StarEngine/src/StarEngine/Physics/SceneQueries.h @@ -0,0 +1,114 @@ +#pragma once + +#include "PhysicsShapes.h" + +namespace StarEngine { + + struct SceneQueryHit + { + uint64_t HitEntity = 0; + glm::vec3 Position = glm::vec3(0.0f); + glm::vec3 Normal = glm::vec3(0.0f); + float Distance = 0.0f; + Ref HitCollider = nullptr; + + void Clear() + { + HitEntity = 0; + Position = glm::vec3(std::numeric_limits::max()); + Normal = glm::vec3(std::numeric_limits::max()); + Distance = std::numeric_limits::max(); + HitCollider = nullptr; + } + }; + + using ExcludedEntityMap = std::unordered_set; + + struct RayCastInfo + { + glm::vec3 Origin; + glm::vec3 Direction; + float MaxDistance; + ExcludedEntityMap ExcludedEntities; + }; + + enum class ShapeCastType { Box, Sphere, Capsule }; + + struct ShapeCastInfo + { + ShapeCastInfo(ShapeCastType castType) + : m_Type(castType) {} + + glm::vec3 Origin = glm::vec3(0.0f); + glm::vec3 Direction = glm::vec3(0.0f); + float MaxDistance = 0.0f; + ExcludedEntityMap ExcludedEntities; + + ShapeCastType GetCastType() const { return m_Type; } + + private: + ShapeCastType m_Type; + }; + + struct BoxCastInfo : public ShapeCastInfo + { + BoxCastInfo() : ShapeCastInfo(ShapeCastType::Box) {} + + glm::vec3 HalfExtent = glm::vec3(0.0f); + }; + + struct SphereCastInfo : public ShapeCastInfo + { + SphereCastInfo() : ShapeCastInfo(ShapeCastType::Sphere) {} + + float Radius = 0.0f; + }; + + struct CapsuleCastInfo : public ShapeCastInfo + { + CapsuleCastInfo() : ShapeCastInfo(ShapeCastType::Capsule) {} + + float HalfHeight = 0.0f; + float Radius = 0.0f; + }; + + struct ShapeOverlapInfo + { + ShapeOverlapInfo(ShapeCastType castType) + : m_Type(castType) + { + } + + glm::vec3 Origin = glm::vec3(0.0f); + + ExcludedEntityMap ExcludedEntities; + + ShapeCastType GetCastType() const { return m_Type; } + + private: + ShapeCastType m_Type; + }; + + struct BoxOverlapInfo : public ShapeOverlapInfo + { + BoxOverlapInfo() : ShapeOverlapInfo(ShapeCastType::Box) {} + + glm::vec3 HalfExtent = glm::vec3(0.0f); + }; + + struct SphereOverlapInfo : public ShapeOverlapInfo + { + SphereOverlapInfo() : ShapeOverlapInfo(ShapeCastType::Sphere) {} + + float Radius = 0.0f; + }; + + struct CapsuleOverlapInfo : public ShapeOverlapInfo + { + CapsuleOverlapInfo() : ShapeOverlapInfo(ShapeCastType::Capsule) {} + + float HalfHeight = 0.0f; + float Radius = 0.0f; + }; + +} diff --git a/StarEngine/src/StarEngine/Physics2D/ContactListener2D.cpp b/StarEngine/src/StarEngine/Physics2D/ContactListener2D.cpp new file mode 100644 index 00000000..8754fcec --- /dev/null +++ b/StarEngine/src/StarEngine/Physics2D/ContactListener2D.cpp @@ -0,0 +1,66 @@ +#include "sepch.h" +#include "ContactListener2D.h" +#include "StarEngine/Scene/Scene.h" + +namespace StarEngine { + + void ContactListener2D::BeginContact(b2Contact* contact) + { + /*Ref scene = ScriptEngine::GetSceneContext(); + + if (!scene->IsPlaying()) + return; + + UUID aID = (UUID)contact->GetFixtureA()->GetBody()->GetUserData().pointer; + UUID bID = (UUID)contact->GetFixtureB()->GetBody()->GetUserData().pointer; + + Entity a = scene->GetEntityWithUUID(aID); + Entity b = scene->GetEntityWithUUID(bID); + + auto callOnCollision2DBegin = [](Entity entity, Entity other) + { + if (!entity.HasComponent()) + return; + + const auto& sc = entity.GetComponent(); + if (!ScriptEngine::IsModuleValid(sc.ScriptClassHandle) || !ScriptEngine::IsEntityInstantiated(entity)) + return; + + ScriptEngine::CallMethod(sc.ManagedInstance, "OnCollision2DBeginInternal", other.GetUUID()); + }; + + callOnCollision2DBegin(a, b); + callOnCollision2DBegin(b, a);*/ + } + + void ContactListener2D::EndContact(b2Contact* contact) + { + /*Ref scene = ScriptEngine::GetSceneContext(); + + if (!scene->IsPlaying()) + return; + + UUID aID = (UUID)contact->GetFixtureA()->GetBody()->GetUserData().pointer; + UUID bID = (UUID)contact->GetFixtureB()->GetBody()->GetUserData().pointer; + + Entity a = scene->GetEntityWithUUID(aID); + Entity b = scene->GetEntityWithUUID(bID); + + auto callOnCollision2DEnd = [](Entity entity, Entity other) + { + if (!entity.HasComponent()) + return; + + const auto& sc = entity.GetComponent(); + if (!ScriptEngine::IsModuleValid(sc.ScriptClassHandle) || !ScriptEngine::IsEntityInstantiated(entity)) + return; + + ScriptEngine::CallMethod(sc.ManagedInstance, "OnCollision2DEndInternal", other.GetUUID()); + }; + + callOnCollision2DEnd(a, b); + callOnCollision2DEnd(b, a);*/ + } + +} + diff --git a/StarEngine/src/StarEngine/Physics/ContactListener2D.h b/StarEngine/src/StarEngine/Physics2D/ContactListener2D.h similarity index 55% rename from StarEngine/src/StarEngine/Physics/ContactListener2D.h rename to StarEngine/src/StarEngine/Physics2D/ContactListener2D.h index b141f80a..d8c61b11 100644 --- a/StarEngine/src/StarEngine/Physics/ContactListener2D.h +++ b/StarEngine/src/StarEngine/Physics2D/ContactListener2D.h @@ -1,55 +1,15 @@ #pragma once -#include "StarEngine/Scene/Components.h" +#include "StarEngine/Scene/Scene.h" -#include "box2d/b2_body.h" - -// Box2D -#include "box2d/box2d.h" -#include "box2d/b2_world.h" -#include "box2d/b2_body.h" -#include "box2d/b2_fixture.h" -#include "box2d/b2_polygon_shape.h" -#include "box2d/b2_circle_shape.h" +#include namespace StarEngine { - namespace Utils { - - inline b2BodyType RigidBody2DTypeToBox2DBody(RigidBody2DComponent::BodyType bodyType) - { - switch (bodyType) - { - case RigidBody2DComponent::BodyType::Static: return b2_staticBody; - case RigidBody2DComponent::BodyType::Dynamic: return b2_dynamicBody; - case RigidBody2DComponent::BodyType::Kinematic: return b2_kinematicBody; - } - - SE_CORE_ASSERT(false, "Unknown body type"); - return b2_staticBody; - } - - inline RigidBody2DComponent::BodyType RigidBody2DTypeFromBox2DBody(b2BodyType bodyType) - { - switch (bodyType) - { - case b2_staticBody: return RigidBody2DComponent::BodyType::Static; - case b2_dynamicBody: return RigidBody2DComponent::BodyType::Dynamic; - case b2_kinematicBody: return RigidBody2DComponent::BodyType::Kinematic; - } - - SE_CORE_ASSERT(false, "Unknown body type"); - return RigidBody2DComponent::BodyType::Static; - } - - } - class ContactListener2D : public b2ContactListener { public: virtual void BeginContact(b2Contact* contact) override; - - /// Called when two fixtures cease to touch. virtual void EndContact(b2Contact* contact) override; /// This is called after a contact is updated. This allows you to inspect a @@ -79,8 +39,5 @@ namespace StarEngine { B2_NOT_USED(contact); B2_NOT_USED(impulse); } - - inline static bool m_IsPlaying = false; }; - } diff --git a/StarEngine/src/StarEngine/Physics2D/Physics2D.cpp b/StarEngine/src/StarEngine/Physics2D/Physics2D.cpp new file mode 100644 index 00000000..e2d3a89c --- /dev/null +++ b/StarEngine/src/StarEngine/Physics2D/Physics2D.cpp @@ -0,0 +1,38 @@ +#include "sepch.h" +#include "Physics2D.h" + +namespace StarEngine { + + std::vector Physics2D::Raycast(Ref scene, const glm::vec2& point0, const glm::vec2& point1) + { + class RayCastCallback : public b2RayCastCallback + { + public: + RayCastCallback(Ref scene, std::vector& results, glm::vec2 p0, glm::vec2 p1) + : m_Scene(scene), m_Results(results), m_Point0(p0), m_Point1(p1) + { + + } + virtual float ReportFixture(b2Fixture* fixture, const b2Vec2& point, + const b2Vec2& normal, float fraction) + { + UUID entityID = *(UUID*)&fixture->GetBody()->GetUserData(); + Entity entity = m_Scene->GetEntityWithUUID(entityID); + float distance = glm::distance(m_Point0, m_Point1) * fraction; + m_Results.emplace_back(entity, glm::vec2(point.x, point.y), glm::vec2(normal.x, normal.y), distance); + return 1.0f; + } + private: + Ref m_Scene; + std::vector& m_Results; + glm::vec2 m_Point0, m_Point1; + }; + + auto& b2dWorld = scene->m_Registry.get(scene->m_SceneEntity).World; + std::vector results; + RayCastCallback callback(scene, results, point0, point1); + b2dWorld->RayCast(&callback, { point0.x, point0.y }, { point1.x, point1.y }); + return results; + } + +} diff --git a/StarEngine/src/StarEngine/Physics2D/Physics2D.h b/StarEngine/src/StarEngine/Physics2D/Physics2D.h new file mode 100644 index 00000000..44023480 --- /dev/null +++ b/StarEngine/src/StarEngine/Physics2D/Physics2D.h @@ -0,0 +1,32 @@ +#pragma once + +#include "StarEngine/Scene/Scene.h" +#include "StarEngine/Scene/Entity.h" +#include "StarEngine/Physics2D/ContactListener2D.h" + +namespace StarEngine { + + struct Raycast2DResult + { + Entity HitEntity; + glm::vec2 Point; + glm::vec2 Normal; + float Distance; // World units + + Raycast2DResult(Entity entity, glm::vec2 point, glm::vec2 normal, float distance) + : HitEntity(entity), Point(point), Normal(normal), Distance(distance) {} + }; + + struct Box2DWorldComponent + { + std::unique_ptr World; + ContactListener2D ContactListener; + }; + + class Physics2D + { + public: + static std::vector Raycast(Ref scene, const glm::vec2& point0, const glm::vec2& point1); + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/DX11/DX11DeviceManager.cpp b/StarEngine/src/StarEngine/Platform/DX11/DX11DeviceManager.cpp new file mode 100644 index 00000000..f0e7dc12 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/DX11/DX11DeviceManager.cpp @@ -0,0 +1,504 @@ +/* +* Copyright (c) 2014-2021, NVIDIA CORPORATION. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +*/ + +/* +License for glfw + +Copyright (c) 2002-2006 Marcus Geelnard + +Copyright (c) 2006-2019 Camilla Lowy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. +*/ +#if 0 +#include "hzpch.h" +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +using nvrhi::RefCountPtr; + +using namespace donut::app; + +class DeviceManager_DX11 : public DeviceManager +{ + RefCountPtr m_DxgiFactory; + RefCountPtr m_DxgiAdapter; + RefCountPtr m_Device; + RefCountPtr m_ImmediateContext; + RefCountPtr m_SwapChain; + DXGI_SWAP_CHAIN_DESC m_SwapChainDesc{}; + HWND m_hWnd = nullptr; + + nvrhi::DeviceHandle m_NvrhiDevice; + nvrhi::TextureHandle m_RhiBackBuffer; + RefCountPtr m_D3D11BackBuffer; + + std::string m_RendererString; + +public: + [[nodiscard]] const char* GetRendererString() const override + { + return m_RendererString.c_str(); + } + + [[nodiscard]] nvrhi::IDevice* GetDevice() const override + { + return m_NvrhiDevice; + } + + void BeginFrame() override; + void ReportLiveObjects() override; + bool EnumerateAdapters(std::vector& outAdapters) override; + + [[nodiscard]] nvrhi::GraphicsAPI GetGraphicsAPI() const override + { + return nvrhi::GraphicsAPI::D3D11; + } +protected: + bool CreateInstanceInternal() override; + bool CreateDevice() override; + bool CreateSwapChain() override; + void DestroyDeviceAndSwapChain() override; + void ResizeSwapChain() override; + void Shutdown() override; + + nvrhi::ITexture* GetCurrentBackBuffer() override + { + return m_RhiBackBuffer; + } + + nvrhi::ITexture* GetBackBuffer(uint32_t index) override + { + if (index == 0) + return m_RhiBackBuffer; + + return nullptr; + } + + uint32_t GetCurrentBackBufferIndex() override + { + return 0; + } + + uint32_t GetBackBufferCount() override + { + return 1; + } + + void Present() override; + + +private: + bool CreateRenderTarget(); + void ReleaseRenderTarget(); +}; + +static bool IsNvDeviceID(UINT id) +{ + return id == 0x10DE; +} + +// Adjust window rect so that it is centred on the given adapter. Clamps to fit if it's too big. +static bool MoveWindowOntoAdapter(IDXGIAdapter* targetAdapter, RECT& rect) +{ + assert(targetAdapter != NULL); + + HRESULT hres = S_OK; + unsigned int outputNo = 0; + while (SUCCEEDED(hres)) + { + nvrhi::RefCountPtr pOutput; + hres = targetAdapter->EnumOutputs(outputNo++, &pOutput); + + if (SUCCEEDED(hres) && pOutput) + { + DXGI_OUTPUT_DESC OutputDesc; + pOutput->GetDesc(&OutputDesc); + const RECT desktop = OutputDesc.DesktopCoordinates; + const int centreX = (int)desktop.left + (int)(desktop.right - desktop.left) / 2; + const int centreY = (int)desktop.top + (int)(desktop.bottom - desktop.top) / 2; + const int winW = rect.right - rect.left; + const int winH = rect.bottom - rect.top; + const int left = centreX - winW / 2; + const int right = left + winW; + const int top = centreY - winH / 2; + const int bottom = top + winH; + rect.left = std::max(left, (int)desktop.left); + rect.right = std::min(right, (int)desktop.right); + rect.bottom = std::min(bottom, (int)desktop.bottom); + rect.top = std::max(top, (int)desktop.top); + + // If there is more than one output, go with the first found. Multi-monitor support could go here. + return true; + } + } + + return false; +} + +void DeviceManager_DX11::BeginFrame() +{ + DXGI_SWAP_CHAIN_DESC newSwapChainDesc; + if (SUCCEEDED(m_SwapChain->GetDesc(&newSwapChainDesc))) + { + if (m_SwapChainDesc.Windowed != newSwapChainDesc.Windowed) + { + BackBufferResizing(); + + m_SwapChainDesc = newSwapChainDesc; + m_DeviceParams.backBufferWidth = newSwapChainDesc.BufferDesc.Width; + m_DeviceParams.backBufferHeight = newSwapChainDesc.BufferDesc.Height; + + if (newSwapChainDesc.Windowed) + glfwSetWindowMonitor(m_Window, nullptr, 50, 50, newSwapChainDesc.BufferDesc.Width, newSwapChainDesc.BufferDesc.Height, 0); + + ResizeSwapChain(); + BackBufferResized(); + } + + } +} + +void DeviceManager_DX11::ReportLiveObjects() +{ + nvrhi::RefCountPtr pDebug; + DXGIGetDebugInterface1(0, IID_PPV_ARGS(&pDebug)); + + if (pDebug) + pDebug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_DETAIL); +} + +static std::string GetAdapterName(DXGI_ADAPTER_DESC const& aDesc) +{ + size_t length = wcsnlen(aDesc.Description, _countof(aDesc.Description)); + + std::string name; + name.resize(length); + WideCharToMultiByte(CP_ACP, 0, aDesc.Description, int(length), name.data(), int(name.size()), nullptr, nullptr); + + return name; +} + +bool DeviceManager_DX11::CreateInstanceInternal() +{ + if (!m_DxgiFactory) + { + HRESULT hres = CreateDXGIFactory1(IID_PPV_ARGS(&m_DxgiFactory)); + if (hres != S_OK) + { + donut::log::error("ERROR in CreateDXGIFactory1.\n" + "For more info, get log from debug D3D runtime: (1) Install DX SDK, and enable Debug D3D from DX Control Panel Utility. (2) Install and start DbgView. (3) Try running the program again.\n"); + return false; + } + } + + return true; +} + +bool DeviceManager_DX11::EnumerateAdapters(std::vector& outAdapters) +{ + if (!m_DxgiFactory) + return false; + + outAdapters.clear(); + + while (true) + { + RefCountPtr adapter; + HRESULT hr = m_DxgiFactory->EnumAdapters(uint32_t(outAdapters.size()), &adapter); + if (FAILED(hr)) + return true; + + DXGI_ADAPTER_DESC desc; + hr = adapter->GetDesc(&desc); + if (FAILED(hr)) + return false; + + AdapterInfo adapterInfo; + + adapterInfo.name = GetAdapterName(desc); + adapterInfo.dxgiAdapter = adapter; + adapterInfo.vendorID = desc.VendorId; + adapterInfo.deviceID = desc.DeviceId; + adapterInfo.dedicatedVideoMemory = desc.DedicatedVideoMemory; + + outAdapters.push_back(std::move(adapterInfo)); + } +} + +bool DeviceManager_DX11::CreateDevice() +{ + int adapterIndex = m_DeviceParams.adapterIndex; + if (adapterIndex < 0) + adapterIndex = 0; + + if (FAILED(m_DxgiFactory->EnumAdapters(adapterIndex, &m_DxgiAdapter))) + { + if (adapterIndex == 0) + donut::log::error("Cannot find any DXGI adapters in the system."); + else + donut::log::error("The specified DXGI adapter %d does not exist.", adapterIndex); + return false; + } + + { + DXGI_ADAPTER_DESC aDesc; + m_DxgiAdapter->GetDesc(&aDesc); + + m_RendererString = GetAdapterName(aDesc); + m_IsNvidia = IsNvDeviceID(aDesc.VendorId); + } + + UINT createFlags = 0; + if (m_DeviceParams.enableDebugRuntime) + createFlags |= D3D11_CREATE_DEVICE_DEBUG; + + const HRESULT hr = D3D11CreateDevice( + m_DxgiAdapter, // pAdapter + D3D_DRIVER_TYPE_UNKNOWN, // DriverType + nullptr, // Software + createFlags, // Flags + &m_DeviceParams.featureLevel, // pFeatureLevels + 1, // FeatureLevels + D3D11_SDK_VERSION, // SDKVersion + &m_Device, // ppDevice + nullptr, // pFeatureLevel + &m_ImmediateContext // ppImmediateContext + ); + + if (FAILED(hr)) + { + return false; + } + + nvrhi::d3d11::DeviceDesc deviceDesc; + deviceDesc.messageCallback = &DefaultMessageCallback::GetInstance(); + deviceDesc.context = m_ImmediateContext; + + m_NvrhiDevice = nvrhi::d3d11::createDevice(deviceDesc); + + if (m_DeviceParams.enableNvrhiValidationLayer) + { + m_NvrhiDevice = nvrhi::validation::createValidationLayer(m_NvrhiDevice); + } + + return true; +} + +bool DeviceManager_DX11::CreateSwapChain() +{ + UINT windowStyle = m_DeviceParams.startFullscreen + ? (WS_POPUP | WS_SYSMENU | WS_VISIBLE) + : m_DeviceParams.startMaximized + ? (WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_MAXIMIZE) + : (WS_OVERLAPPEDWINDOW | WS_VISIBLE); + + RECT rect = { 0, 0, LONG(m_DeviceParams.backBufferWidth), LONG(m_DeviceParams.backBufferHeight) }; + AdjustWindowRect(&rect, windowStyle, FALSE); + + if (MoveWindowOntoAdapter(m_DxgiAdapter, rect)) + { + glfwSetWindowPos(m_Window, rect.left, rect.top); + } + + m_hWnd = glfwGetWin32Window(m_Window); + + RECT clientRect; + GetClientRect(m_hWnd, &clientRect); + UINT width = clientRect.right - clientRect.left; + UINT height = clientRect.bottom - clientRect.top; + + ZeroMemory(&m_SwapChainDesc, sizeof(m_SwapChainDesc)); + m_SwapChainDesc.BufferCount = m_DeviceParams.swapChainBufferCount; + m_SwapChainDesc.BufferDesc.Width = width; + m_SwapChainDesc.BufferDesc.Height = height; + m_SwapChainDesc.BufferDesc.RefreshRate.Numerator = m_DeviceParams.refreshRate; + m_SwapChainDesc.BufferDesc.RefreshRate.Denominator = 0; + m_SwapChainDesc.BufferUsage = m_DeviceParams.swapChainUsage; + m_SwapChainDesc.OutputWindow = m_hWnd; + m_SwapChainDesc.SampleDesc.Count = m_DeviceParams.swapChainSampleCount; + m_SwapChainDesc.SampleDesc.Quality = m_DeviceParams.swapChainSampleQuality; + m_SwapChainDesc.Windowed = !m_DeviceParams.startFullscreen; + m_SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + m_SwapChainDesc.Flags = m_DeviceParams.allowModeSwitch ? DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH : 0; + + // Special processing for sRGB swap chain formats. + // DXGI will not create a swap chain with an sRGB format, but its contents will be interpreted as sRGB. + // So we need to use a non-sRGB format here, but store the true sRGB format for later framebuffer creation. + switch (m_DeviceParams.swapChainFormat) // NOLINT(clang-diagnostic-switch-enum) + { + case nvrhi::Format::SRGBA8_UNORM: + m_SwapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + break; + case nvrhi::Format::SBGRA8_UNORM: + m_SwapChainDesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + break; + default: + m_SwapChainDesc.BufferDesc.Format = nvrhi::d3d11::convertFormat(m_DeviceParams.swapChainFormat); + break; + } + + HRESULT hr = m_DxgiFactory->CreateSwapChain(m_Device, &m_SwapChainDesc, &m_SwapChain); + + if(FAILED(hr)) + { + donut::log::error("Failed to create a swap chain, HRESULT = 0x%08x", hr); + return false; + } + + bool ret = CreateRenderTarget(); + + if(!ret) + { + return false; + } + + return true; +} + +void DeviceManager_DX11::DestroyDeviceAndSwapChain() +{ + m_RhiBackBuffer = nullptr; + m_NvrhiDevice = nullptr; + + if (m_SwapChain) + { + m_SwapChain->SetFullscreenState(false, nullptr); + } + + ReleaseRenderTarget(); + + m_SwapChain = nullptr; + m_ImmediateContext = nullptr; + m_Device = nullptr; +} + +bool DeviceManager_DX11::CreateRenderTarget() +{ + ReleaseRenderTarget(); + + const HRESULT hr = m_SwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&m_D3D11BackBuffer); // NOLINT(clang-diagnostic-language-extension-token) + if (FAILED(hr)) + { + return false; + } + + nvrhi::TextureDesc textureDesc; + textureDesc.width = m_DeviceParams.backBufferWidth; + textureDesc.height = m_DeviceParams.backBufferHeight; + textureDesc.sampleCount = m_DeviceParams.swapChainSampleCount; + textureDesc.sampleQuality = m_DeviceParams.swapChainSampleQuality; + textureDesc.format = m_DeviceParams.swapChainFormat; + textureDesc.debugName = "SwapChainBuffer"; + textureDesc.isRenderTarget = true; + textureDesc.isUAV = false; + + m_RhiBackBuffer = m_NvrhiDevice->createHandleForNativeTexture(nvrhi::ObjectTypes::D3D11_Resource, static_cast(m_D3D11BackBuffer.Get()), textureDesc); + + if (FAILED(hr)) + { + return false; + } + + return true; +} + +void DeviceManager_DX11::ReleaseRenderTarget() +{ + m_RhiBackBuffer = nullptr; + m_D3D11BackBuffer = nullptr; +} + +void DeviceManager_DX11::ResizeSwapChain() +{ + ReleaseRenderTarget(); + + if (!m_SwapChain) + return; + + const HRESULT hr = m_SwapChain->ResizeBuffers(m_DeviceParams.swapChainBufferCount, + m_DeviceParams.backBufferWidth, + m_DeviceParams.backBufferHeight, + m_SwapChainDesc.BufferDesc.Format, + m_SwapChainDesc.Flags); + + if (FAILED(hr)) + { + donut::log::fatal("ResizeBuffers failed"); + } + + const bool ret = CreateRenderTarget(); + if (!ret) + { + donut::log::fatal("CreateRenderTarget failed"); + } +} + +void DeviceManager_DX11::Shutdown() +{ + DeviceManager::Shutdown(); + + if (m_DeviceParams.enableDebugRuntime) + { + ReportLiveObjects(); + } +} + +void DeviceManager_DX11::Present() +{ + m_SwapChain->Present(m_DeviceParams.vsyncEnabled ? 1 : 0, 0); +} + +DeviceManager *DeviceManager::CreateD3D11() +{ + return new DeviceManager_DX11(); +} + +#endif diff --git a/StarEngine/src/StarEngine/Platform/DX12/DX12DeviceManager.cpp b/StarEngine/src/StarEngine/Platform/DX12/DX12DeviceManager.cpp new file mode 100644 index 00000000..47682d56 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/DX12/DX12DeviceManager.cpp @@ -0,0 +1,642 @@ +/* +* Copyright (c) 2014-2021, NVIDIA CORPORATION. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +*/ + +/* +License for glfw + +Copyright (c) 2002-2006 Marcus Geelnard + +Copyright (c) 2006-2019 Camilla Lowy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. +*/ +#if 0 +#include "hzpch.h" +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include + +using nvrhi::RefCountPtr; + +using namespace donut::app; + +#define HR_RETURN(hr) if(FAILED(hr)) return false; + +class DeviceManager_DX12 : public DeviceManager +{ + RefCountPtr m_DxgiFactory2; + RefCountPtr m_Device12; + RefCountPtr m_GraphicsQueue; + RefCountPtr m_ComputeQueue; + RefCountPtr m_CopyQueue; + RefCountPtr m_SwapChain; + DXGI_SWAP_CHAIN_DESC1 m_SwapChainDesc{}; + DXGI_SWAP_CHAIN_FULLSCREEN_DESC m_FullScreenDesc{}; + RefCountPtr m_DxgiAdapter; + HWND m_hWnd = nullptr; + bool m_TearingSupported = false; + + std::vector> m_SwapChainBuffers; + std::vector m_RhiSwapChainBuffers; + RefCountPtr m_FrameFence; + std::vector m_FrameFenceEvents; + + UINT64 m_FrameCount = 1; + + nvrhi::DeviceHandle m_NvrhiDevice; + + std::string m_RendererString; + +public: + const char *GetRendererString() const override + { + return m_RendererString.c_str(); + } + + nvrhi::IDevice *GetDevice() const override + { + return m_NvrhiDevice; + } + + void ReportLiveObjects() override; + bool EnumerateAdapters(std::vector& outAdapters) override; + + nvrhi::GraphicsAPI GetGraphicsAPI() const override + { + return nvrhi::GraphicsAPI::D3D12; + } + +protected: + bool CreateInstanceInternal() override; + bool CreateDevice() override; + bool CreateSwapChain() override; + void DestroyDeviceAndSwapChain() override; + void ResizeSwapChain() override; + nvrhi::ITexture* GetCurrentBackBuffer() override; + nvrhi::ITexture* GetBackBuffer(uint32_t index) override; + uint32_t GetCurrentBackBufferIndex() override; + uint32_t GetBackBufferCount() override; + void BeginFrame() override; + void Present() override; + void Shutdown() override; + +private: + bool CreateRenderTargets(); + void ReleaseRenderTargets(); +}; + +static bool IsNvDeviceID(UINT id) +{ + return id == 0x10DE; +} + +// Adjust window rect so that it is centred on the given adapter. Clamps to fit if it's too big. +static bool MoveWindowOntoAdapter(IDXGIAdapter* targetAdapter, RECT& rect) +{ + assert(targetAdapter != NULL); + + HRESULT hres = S_OK; + unsigned int outputNo = 0; + while (SUCCEEDED(hres)) + { + nvrhi::RefCountPtr pOutput; + hres = targetAdapter->EnumOutputs(outputNo++, &pOutput); + + if (SUCCEEDED(hres) && pOutput) + { + DXGI_OUTPUT_DESC OutputDesc; + pOutput->GetDesc(&OutputDesc); + const RECT desktop = OutputDesc.DesktopCoordinates; + const int centreX = (int)desktop.left + (int)(desktop.right - desktop.left) / 2; + const int centreY = (int)desktop.top + (int)(desktop.bottom - desktop.top) / 2; + const int winW = rect.right - rect.left; + const int winH = rect.bottom - rect.top; + const int left = centreX - winW / 2; + const int right = left + winW; + const int top = centreY - winH / 2; + const int bottom = top + winH; + rect.left = std::max(left, (int)desktop.left); + rect.right = std::min(right, (int)desktop.right); + rect.bottom = std::min(bottom, (int)desktop.bottom); + rect.top = std::max(top, (int)desktop.top); + + // If there is more than one output, go with the first found. Multi-monitor support could go here. + return true; + } + } + + return false; +} + +void DeviceManager_DX12::ReportLiveObjects() +{ + nvrhi::RefCountPtr pDebug; + DXGIGetDebugInterface1(0, IID_PPV_ARGS(&pDebug)); + + if (pDebug) + { + DXGI_DEBUG_RLO_FLAGS flags = (DXGI_DEBUG_RLO_FLAGS)(DXGI_DEBUG_RLO_IGNORE_INTERNAL | DXGI_DEBUG_RLO_SUMMARY | DXGI_DEBUG_RLO_DETAIL); + HRESULT hr = pDebug->ReportLiveObjects(DXGI_DEBUG_ALL, flags); + if (FAILED(hr)) + { + donut::log::error("ReportLiveObjects failed, HRESULT = 0x%08x", hr); + } + } +} + +static std::string GetAdapterName(DXGI_ADAPTER_DESC const& aDesc) +{ + size_t length = wcsnlen(aDesc.Description, _countof(aDesc.Description)); + + std::string name; + name.resize(length); + WideCharToMultiByte(CP_ACP, 0, aDesc.Description, int(length), name.data(), int(name.size()), nullptr, nullptr); + + return name; +} + +bool DeviceManager_DX12::CreateInstanceInternal() +{ + if (!m_DxgiFactory2) + { + HRESULT hres = CreateDXGIFactory2(m_DeviceParams.enableDebugRuntime ? DXGI_CREATE_FACTORY_DEBUG : 0, IID_PPV_ARGS(&m_DxgiFactory2)); + if (hres != S_OK) + { + donut::log::error("ERROR in CreateDXGIFactory2.\n" + "For more info, get log from debug D3D runtime: (1) Install DX SDK, and enable Debug D3D from DX Control Panel Utility. (2) Install and start DbgView. (3) Try running the program again.\n"); + return false; + } + } + + return true; +} + +bool DeviceManager_DX12::EnumerateAdapters(std::vector& outAdapters) +{ + if (!m_DxgiFactory2) + return false; + + outAdapters.clear(); + + while (true) + { + RefCountPtr adapter; + HRESULT hr = m_DxgiFactory2->EnumAdapters(uint32_t(outAdapters.size()), &adapter); + if (FAILED(hr)) + return true; + + DXGI_ADAPTER_DESC desc; + hr = adapter->GetDesc(&desc); + if (FAILED(hr)) + return false; + + AdapterInfo adapterInfo; + + adapterInfo.name = GetAdapterName(desc); + adapterInfo.dxgiAdapter = adapter; + adapterInfo.vendorID = desc.VendorId; + adapterInfo.deviceID = desc.DeviceId; + adapterInfo.dedicatedVideoMemory = desc.DedicatedVideoMemory; + + outAdapters.push_back(std::move(adapterInfo)); + } +} + +bool DeviceManager_DX12::CreateDevice() +{ + if (m_DeviceParams.enableDebugRuntime) + { + RefCountPtr pDebug; + HRESULT hr = D3D12GetDebugInterface(IID_PPV_ARGS(&pDebug)); + HR_RETURN(hr) + + pDebug->EnableDebugLayer(); + } + + int adapterIndex = m_DeviceParams.adapterIndex; + if (adapterIndex < 0) + adapterIndex = 0; + + if (FAILED(m_DxgiFactory2->EnumAdapters(adapterIndex, &m_DxgiAdapter))) + { + if (adapterIndex == 0) + donut::log::error("Cannot find any DXGI adapters in the system."); + else + donut::log::error("The specified DXGI adapter %d does not exist.", adapterIndex); + return false; + } + + { + DXGI_ADAPTER_DESC aDesc; + m_DxgiAdapter->GetDesc(&aDesc); + + m_RendererString = GetAdapterName(aDesc); + m_IsNvidia = IsNvDeviceID(aDesc.VendorId); + } + + + HRESULT hr = D3D12CreateDevice( + m_DxgiAdapter, + m_DeviceParams.featureLevel, + IID_PPV_ARGS(&m_Device12)); + HR_RETURN(hr) + + if (m_DeviceParams.enableDebugRuntime) + { + RefCountPtr pInfoQueue; + m_Device12->QueryInterface(&pInfoQueue); + + if (pInfoQueue) + { +#ifdef _DEBUG + pInfoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, true); + pInfoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, true); +#endif + + D3D12_MESSAGE_ID disableMessageIDs[] = { + D3D12_MESSAGE_ID_CLEARDEPTHSTENCILVIEW_MISMATCHINGCLEARVALUE, + D3D12_MESSAGE_ID_COMMAND_LIST_STATIC_DESCRIPTOR_RESOURCE_DIMENSION_MISMATCH, // descriptor validation doesn't understand acceleration structures + }; + + D3D12_INFO_QUEUE_FILTER filter = {}; + filter.DenyList.pIDList = disableMessageIDs; + filter.DenyList.NumIDs = sizeof(disableMessageIDs) / sizeof(disableMessageIDs[0]); + pInfoQueue->AddStorageFilterEntries(&filter); + } + } + + D3D12_COMMAND_QUEUE_DESC queueDesc; + ZeroMemory(&queueDesc, sizeof(queueDesc)); + queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + queueDesc.NodeMask = 1; + hr = m_Device12->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_GraphicsQueue)); + HR_RETURN(hr) + m_GraphicsQueue->SetName(L"Graphics Queue"); + + if (m_DeviceParams.enableComputeQueue) + { + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_COMPUTE; + hr = m_Device12->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_ComputeQueue)); + HR_RETURN(hr) + m_ComputeQueue->SetName(L"Compute Queue"); + } + + if (m_DeviceParams.enableCopyQueue) + { + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_COPY; + hr = m_Device12->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_CopyQueue)); + HR_RETURN(hr) + m_CopyQueue->SetName(L"Copy Queue"); + } + + nvrhi::d3d12::DeviceDesc deviceDesc; + deviceDesc.errorCB = &DefaultMessageCallback::GetInstance(); + deviceDesc.pDevice = m_Device12; + deviceDesc.pGraphicsCommandQueue = m_GraphicsQueue; + deviceDesc.pComputeCommandQueue = m_ComputeQueue; + deviceDesc.pCopyCommandQueue = m_CopyQueue; + + m_NvrhiDevice = nvrhi::d3d12::createDevice(deviceDesc); + + if (m_DeviceParams.enableNvrhiValidationLayer) + { + m_NvrhiDevice = nvrhi::validation::createValidationLayer(m_NvrhiDevice); + } + + return true; +} + +bool DeviceManager_DX12::CreateSwapChain() +{ + UINT windowStyle = m_DeviceParams.startFullscreen + ? (WS_POPUP | WS_SYSMENU | WS_VISIBLE) + : m_DeviceParams.startMaximized + ? (WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_MAXIMIZE) + : (WS_OVERLAPPEDWINDOW | WS_VISIBLE); + + RECT rect = { 0, 0, LONG(m_DeviceParams.backBufferWidth), LONG(m_DeviceParams.backBufferHeight) }; + AdjustWindowRect(&rect, windowStyle, FALSE); + + if (MoveWindowOntoAdapter(m_DxgiAdapter, rect)) + { + glfwSetWindowPos(m_Window, rect.left, rect.top); + } + + m_hWnd = glfwGetWin32Window(m_Window); + + HRESULT hr = E_FAIL; + + RECT clientRect; + GetClientRect(m_hWnd, &clientRect); + UINT width = clientRect.right - clientRect.left; + UINT height = clientRect.bottom - clientRect.top; + + ZeroMemory(&m_SwapChainDesc, sizeof(m_SwapChainDesc)); + m_SwapChainDesc.Width = width; + m_SwapChainDesc.Height = height; + m_SwapChainDesc.SampleDesc.Count = m_DeviceParams.swapChainSampleCount; + m_SwapChainDesc.SampleDesc.Quality = 0; + m_SwapChainDesc.BufferUsage = m_DeviceParams.swapChainUsage; + m_SwapChainDesc.BufferCount = m_DeviceParams.swapChainBufferCount; + m_SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + m_SwapChainDesc.Flags = m_DeviceParams.allowModeSwitch ? DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH : 0; + + // Special processing for sRGB swap chain formats. + // DXGI will not create a swap chain with an sRGB format, but its contents will be interpreted as sRGB. + // So we need to use a non-sRGB format here, but store the true sRGB format for later framebuffer creation. + switch (m_DeviceParams.swapChainFormat) // NOLINT(clang-diagnostic-switch-enum) + { + case nvrhi::Format::SRGBA8_UNORM: + m_SwapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + break; + case nvrhi::Format::SBGRA8_UNORM: + m_SwapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + break; + default: + m_SwapChainDesc.Format = nvrhi::d3d12::convertFormat(m_DeviceParams.swapChainFormat); + break; + } + + RefCountPtr pDxgiFactory5; + if (SUCCEEDED(m_DxgiFactory2->QueryInterface(IID_PPV_ARGS(&pDxgiFactory5)))) + { + BOOL supported = 0; + if (SUCCEEDED(pDxgiFactory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &supported, sizeof(supported)))) + m_TearingSupported = (supported != 0); + } + + if (m_TearingSupported) + { + m_SwapChainDesc.Flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; + } + + m_FullScreenDesc = {}; + m_FullScreenDesc.RefreshRate.Numerator = m_DeviceParams.refreshRate; + m_FullScreenDesc.RefreshRate.Denominator = 1; + m_FullScreenDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE; + m_FullScreenDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; + m_FullScreenDesc.Windowed = !m_DeviceParams.startFullscreen; + + RefCountPtr pSwapChain1; + hr = m_DxgiFactory2->CreateSwapChainForHwnd(m_GraphicsQueue, m_hWnd, &m_SwapChainDesc, &m_FullScreenDesc, nullptr, &pSwapChain1); + HR_RETURN(hr) + + hr = pSwapChain1->QueryInterface(IID_PPV_ARGS(&m_SwapChain)); + HR_RETURN(hr) + + if (!CreateRenderTargets()) + return false; + + hr = m_Device12->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_FrameFence)); + HR_RETURN(hr) + + for(UINT bufferIndex = 0; bufferIndex < m_SwapChainDesc.BufferCount; bufferIndex++) + { + m_FrameFenceEvents.push_back( CreateEvent(nullptr, false, true, nullptr) ); + } + + return true; +} + +void DeviceManager_DX12::DestroyDeviceAndSwapChain() +{ + m_RhiSwapChainBuffers.clear(); + m_RendererString.clear(); + + ReleaseRenderTargets(); + + m_NvrhiDevice = nullptr; + + for (auto fenceEvent : m_FrameFenceEvents) + { + WaitForSingleObject(fenceEvent, INFINITE); + CloseHandle(fenceEvent); + } + + m_FrameFenceEvents.clear(); + + if (m_SwapChain) + { + m_SwapChain->SetFullscreenState(false, nullptr); + } + + m_SwapChainBuffers.clear(); + + m_FrameFence = nullptr; + m_SwapChain = nullptr; + m_GraphicsQueue = nullptr; + m_ComputeQueue = nullptr; + m_CopyQueue = nullptr; + m_Device12 = nullptr; +} + +bool DeviceManager_DX12::CreateRenderTargets() +{ + m_SwapChainBuffers.resize(m_SwapChainDesc.BufferCount); + m_RhiSwapChainBuffers.resize(m_SwapChainDesc.BufferCount); + + for(UINT n = 0; n < m_SwapChainDesc.BufferCount; n++) + { + const HRESULT hr = m_SwapChain->GetBuffer(n, IID_PPV_ARGS(&m_SwapChainBuffers[n])); + HR_RETURN(hr) + + nvrhi::TextureDesc textureDesc; + textureDesc.width = m_DeviceParams.backBufferWidth; + textureDesc.height = m_DeviceParams.backBufferHeight; + textureDesc.sampleCount = m_DeviceParams.swapChainSampleCount; + textureDesc.sampleQuality = m_DeviceParams.swapChainSampleQuality; + textureDesc.format = m_DeviceParams.swapChainFormat; + textureDesc.debugName = "SwapChainBuffer"; + textureDesc.isRenderTarget = true; + textureDesc.isUAV = false; + textureDesc.initialState = nvrhi::ResourceStates::Present; + textureDesc.keepInitialState = true; + + m_RhiSwapChainBuffers[n] = m_NvrhiDevice->createHandleForNativeTexture(nvrhi::ObjectTypes::D3D12_Resource, nvrhi::Object(m_SwapChainBuffers[n]), textureDesc); + } + + return true; +} + +void DeviceManager_DX12::ReleaseRenderTargets() +{ + // Make sure that all frames have finished rendering + m_NvrhiDevice->waitForIdle(); + + // Release all in-flight references to the render targets + m_NvrhiDevice->runGarbageCollection(); + + // Set the events so that WaitForSingleObject in OneFrame will not hang later + for(auto e : m_FrameFenceEvents) + SetEvent(e); + + // Release the old buffers because ResizeBuffers requires that + m_RhiSwapChainBuffers.clear(); + m_SwapChainBuffers.clear(); +} + +void DeviceManager_DX12::ResizeSwapChain() +{ + ReleaseRenderTargets(); + + if (!m_NvrhiDevice) + return; + + if (!m_SwapChain) + return; + + const HRESULT hr = m_SwapChain->ResizeBuffers(m_DeviceParams.swapChainBufferCount, + m_DeviceParams.backBufferWidth, + m_DeviceParams.backBufferHeight, + m_SwapChainDesc.Format, + m_SwapChainDesc.Flags); + + if (FAILED(hr)) + { + donut::log::fatal("ResizeBuffers failed"); + } + + bool ret = CreateRenderTargets(); + if (!ret) + { + donut::log::fatal("CreateRenderTarget failed"); + } +} + +void DeviceManager_DX12::BeginFrame() +{ + DXGI_SWAP_CHAIN_DESC1 newSwapChainDesc; + DXGI_SWAP_CHAIN_FULLSCREEN_DESC newFullScreenDesc; + if (SUCCEEDED(m_SwapChain->GetDesc1(&newSwapChainDesc)) && SUCCEEDED(m_SwapChain->GetFullscreenDesc(&newFullScreenDesc))) + { + if (m_FullScreenDesc.Windowed != newFullScreenDesc.Windowed) + { + BackBufferResizing(); + + m_FullScreenDesc = newFullScreenDesc; + m_SwapChainDesc = newSwapChainDesc; + m_DeviceParams.backBufferWidth = newSwapChainDesc.Width; + m_DeviceParams.backBufferHeight = newSwapChainDesc.Height; + + if(newFullScreenDesc.Windowed) + glfwSetWindowMonitor(m_Window, nullptr, 50, 50, newSwapChainDesc.Width, newSwapChainDesc.Height, 0); + + ResizeSwapChain(); + BackBufferResized(); + } + + } + + auto bufferIndex = m_SwapChain->GetCurrentBackBufferIndex(); + + WaitForSingleObject(m_FrameFenceEvents[bufferIndex], INFINITE); +} + +nvrhi::ITexture* DeviceManager_DX12::GetCurrentBackBuffer() +{ + return m_RhiSwapChainBuffers[m_SwapChain->GetCurrentBackBufferIndex()]; +} + +nvrhi::ITexture* DeviceManager_DX12::GetBackBuffer(uint32_t index) +{ + if (index < m_RhiSwapChainBuffers.size()) + return m_RhiSwapChainBuffers[index]; + return nullptr; +} + +uint32_t DeviceManager_DX12::GetCurrentBackBufferIndex() +{ + return m_SwapChain->GetCurrentBackBufferIndex(); +} + +uint32_t DeviceManager_DX12::GetBackBufferCount() +{ + return m_SwapChainDesc.BufferCount; +} + +void DeviceManager_DX12::Present() +{ + if (!m_windowVisible) + return; + + auto bufferIndex = m_SwapChain->GetCurrentBackBufferIndex(); + + UINT presentFlags = 0; + if (!m_DeviceParams.vsyncEnabled && m_FullScreenDesc.Windowed && m_TearingSupported) + presentFlags |= DXGI_PRESENT_ALLOW_TEARING; + + m_SwapChain->Present(m_DeviceParams.vsyncEnabled ? 1 : 0, presentFlags); + + m_FrameFence->SetEventOnCompletion(m_FrameCount, m_FrameFenceEvents[bufferIndex]); + m_GraphicsQueue->Signal(m_FrameFence, m_FrameCount); + m_FrameCount++; +} + +void DeviceManager_DX12::Shutdown() +{ + DeviceManager::Shutdown(); + + m_DxgiAdapter = nullptr; + m_DxgiFactory2 = nullptr; + + if (m_DeviceParams.enableDebugRuntime) + { + ReportLiveObjects(); + } +} + +DeviceManager *DeviceManager::CreateD3D12(void) +{ + return new DeviceManager_DX12(); +} + +#endif diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathGpuCrashTracker.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathGpuCrashTracker.cpp new file mode 100644 index 00000000..c43c4036 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathGpuCrashTracker.cpp @@ -0,0 +1,351 @@ +//********************************************************* +// +// Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//********************************************************* + +#include "sepch.h" +#include +#include +#include + +#include "NsightAftermathGpuCrashTracker.h" + +#include "StarEngine/Core/Log.h" + +//********************************************************* +// GpuCrashTracker implementation +//********************************************************* + +GpuCrashTracker::GpuCrashTracker() + : m_initialized(false) + , m_mutex() + , m_shaderDebugInfo() + , m_shaderDatabase() +{ +} + +GpuCrashTracker::~GpuCrashTracker() +{ + // If initialized, disable GPU crash dumps + if (m_initialized) + { + GFSDK_Aftermath_DisableGpuCrashDumps(); + } +} + +// Initialize the GPU Crash Dump Tracker +void GpuCrashTracker::Initialize() +{ + // Enable GPU crash dumps and set up the callbacks for crash dump notifications, + // shader debug information notifications, and providing additional crash + // dump description data.Only the crash dump callback is mandatory. The other two + // callbacks are optional and can be omitted, by passing nullptr, if the corresponding + // functionality is not used. + // The DeferDebugInfoCallbacks flag enables caching of shader debug information data + // in memory. If the flag is set, ShaderDebugInfoCallback will be called only + // in the event of a crash, right before GpuCrashDumpCallback. If the flag is not set, + // ShaderDebugInfoCallback will be called for every shader that is compiled. + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_EnableGpuCrashDumps( + GFSDK_Aftermath_Version_API, + GFSDK_Aftermath_GpuCrashDumpWatchedApiFlags_Vulkan, + GFSDK_Aftermath_GpuCrashDumpFeatureFlags_DeferDebugInfoCallbacks, // Let the Nsight Aftermath library cache shader debug information. + GpuCrashDumpCallback, // Register callback for GPU crash dumps. + ShaderDebugInfoCallback, // Register callback for shader debug information. + CrashDumpDescriptionCallback, // Register callback for GPU crash dump description. + this)); // Set the GpuCrashTracker object as user data for the above callbacks. + + SE_CORE_INFO_TAG("Renderer", "GpuCrashTracker::Initialize"); + + m_initialized = true; +} + +// Handler for GPU crash dump callbacks from Nsight Aftermath +void GpuCrashTracker::OnCrashDump(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize) +{ + // Make sure only one thread at a time... + std::lock_guard lock(m_mutex); + + // Write to file for later in-depth analysis with Nsight Graphics. + WriteGpuCrashDumpToFile(pGpuCrashDump, gpuCrashDumpSize); +} + +// Handler for shader debug information callbacks +void GpuCrashTracker::OnShaderDebugInfo(const void* pShaderDebugInfo, const uint32_t shaderDebugInfoSize) +{ + // Make sure only one thread at a time... + std::lock_guard lock(m_mutex); + + // Get shader debug information identifier + GFSDK_Aftermath_ShaderDebugInfoIdentifier identifier = {}; + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GetShaderDebugInfoIdentifier( + GFSDK_Aftermath_Version_API, + pShaderDebugInfo, + shaderDebugInfoSize, + &identifier)); + + // Store information for decoding of GPU crash dumps with shader address mapping + // from within the application. + std::vector data((uint8_t*)pShaderDebugInfo, (uint8_t*)pShaderDebugInfo + shaderDebugInfoSize); + m_shaderDebugInfo[identifier].swap(data); + + // Write to file for later in-depth analysis of crash dumps with Nsight Graphics + WriteShaderDebugInformationToFile(identifier, pShaderDebugInfo, shaderDebugInfoSize); +} + +// Handler for GPU crash dump description callbacks +void GpuCrashTracker::OnDescription(PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription) +{ + // Add some basic description about the crash. This is called after the GPU crash happens, but before + // the actual GPU crash dump callback. The provided data is included in the crash dump and can be + // retrieved using GFSDK_Aftermath_GpuCrashDump_GetDescription(). + addDescription(GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationName, "VkHelloNsightAftermath"); + addDescription(GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationVersion, "v1.0"); + addDescription(GFSDK_Aftermath_GpuCrashDumpDescriptionKey_UserDefined, "This is a GPU crash dump example."); + addDescription(GFSDK_Aftermath_GpuCrashDumpDescriptionKey_UserDefined + 1, "Engine State: Rendering."); + addDescription(GFSDK_Aftermath_GpuCrashDumpDescriptionKey_UserDefined + 2, "More user-defined information..."); +} + +// Helper for writing a GPU crash dump to a file +void GpuCrashTracker::WriteGpuCrashDumpToFile(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize) +{ + // Create a GPU crash dump decoder object for the GPU crash dump. + GFSDK_Aftermath_GpuCrashDump_Decoder decoder = {}; + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_CreateDecoder( + GFSDK_Aftermath_Version_API, + pGpuCrashDump, + gpuCrashDumpSize, + &decoder)); + + // Use the decoder object to read basic information, like application + // name, PID, etc. from the GPU crash dump. + GFSDK_Aftermath_GpuCrashDump_BaseInfo baseInfo = {}; + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetBaseInfo(decoder, &baseInfo)); + + // Use the decoder object to query the application name that was set + // in the GPU crash dump description. + uint32_t applicationNameLength = 0; + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetDescriptionSize( + decoder, + GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationName, + &applicationNameLength)); + + std::vector applicationName(applicationNameLength, '\0'); + + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetDescription( + decoder, + GFSDK_Aftermath_GpuCrashDumpDescriptionKey_ApplicationName, + uint32_t(applicationName.size()), + applicationName.data())); + + // Create a unique file name for writing the crash dump data to a file. + // Note: due to an Nsight Aftermath bug (will be fixed in an upcoming + // driver release) we may see redundant crash dumps. As a workaround, + // attach a unique count to each generated file name. + static int count = 0; + const std::string baseFileName = + std::string(applicationName.data()) + + "-" + + std::to_string(baseInfo.pid) + + "-" + + std::to_string(++count); + + // Write the the crash dump data to a file using the .nv-gpudmp extension + // registered with Nsight Graphics. + const std::string crashDumpFileName = baseFileName + ".nv-gpudmp"; + std::ofstream dumpFile(crashDumpFileName, std::ios::out | std::ios::binary); + if (dumpFile) + { + dumpFile.write((const char*)pGpuCrashDump, gpuCrashDumpSize); + dumpFile.close(); + } + + // Decode the crash dump to a JSON string. + // Step 1: Generate the JSON and get the size. + uint32_t jsonSize = 0; + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GenerateJSON( + decoder, + GFSDK_Aftermath_GpuCrashDumpDecoderFlags_ALL_INFO, + GFSDK_Aftermath_GpuCrashDumpFormatterFlags_NONE, + ShaderDebugInfoLookupCallback, + ShaderLookupCallback, + nullptr, + ShaderSourceDebugInfoLookupCallback, + this, + &jsonSize)); + // Step 2: Allocate a buffer and fetch the generated JSON. + std::vector json(jsonSize); + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_GetJSON( + decoder, + uint32_t(json.size()), + json.data())); + + // Write the the crash dump data as JSON to a file. + const std::string jsonFileName = crashDumpFileName + ".json"; + std::ofstream jsonFile(jsonFileName, std::ios::out | std::ios::binary); + if (jsonFile) + { + jsonFile.write(json.data(), json.size()); + jsonFile.close(); + } + + // Destroy the GPU crash dump decoder object. + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GpuCrashDump_DestroyDecoder(decoder)); +} + +// Helper for writing shader debug information to a file +void GpuCrashTracker::WriteShaderDebugInformationToFile( + GFSDK_Aftermath_ShaderDebugInfoIdentifier identifier, + const void* pShaderDebugInfo, + const uint32_t shaderDebugInfoSize) +{ + // Create a unique file name. + const std::string filePath = "shader-" + std::to_string(identifier) + ".nvdbg"; + + std::ofstream f(filePath, std::ios::out | std::ios::binary); + if (f) + { + f.write((const char*)pShaderDebugInfo, shaderDebugInfoSize); + } +} + +// Handler for shader debug information lookup callbacks. +// This is used by the JSON decoder for mapping shader instruction +// addresses to DXIL lines or HLSl source lines. +void GpuCrashTracker::OnShaderDebugInfoLookup( + const GFSDK_Aftermath_ShaderDebugInfoIdentifier& identifier, + PFN_GFSDK_Aftermath_SetData setShaderDebugInfo) const +{ + // Search the list of shader debug information blobs received earlier. + auto i_debugInfo = m_shaderDebugInfo.find(identifier); + if (i_debugInfo == m_shaderDebugInfo.end()) + { + // Early exit, nothing found. No need to call setShaderDebugInfo. + return; + } + + // Let the GPU crash dump decoder know about the shader debug information + // that was found. + setShaderDebugInfo(i_debugInfo->second.data(), uint32_t(i_debugInfo->second.size())); +} + +// Handler for shader lookup callbacks. +// This is used by the JSON decoder for mapping shader instruction +// addresses to DXIL lines or HLSL source lines. +// NOTE: If the application loads stripped shader binaries (-Qstrip_debug), +// Aftermath will require access to both the stripped and the not stripped +// shader binaries. +void GpuCrashTracker::OnShaderLookup( + const GFSDK_Aftermath_ShaderHash& shaderHash, + PFN_GFSDK_Aftermath_SetData setShaderBinary) const +{ + // Find shader binary data for the shader hash in the shader database. + std::vector shaderBinary; + if (!m_shaderDatabase.FindShaderBinary(shaderHash, shaderBinary)) + { + // Early exit, nothing found. No need to call setShaderBinary. + return; + } + + // Let the GPU crash dump decoder know about the shader data + // that was found. + setShaderBinary(shaderBinary.data(), uint32_t(shaderBinary.size())); +} + +// Handler for shader source debug info lookup callbacks. +// This is used by the JSON decoder for mapping shader instruction addresses to +// HLSL source lines, if the shaders used by the application were compiled with +// separate debug info data files. +void GpuCrashTracker::OnShaderSourceDebugInfoLookup( + const GFSDK_Aftermath_ShaderDebugName& shaderDebugName, + PFN_GFSDK_Aftermath_SetData setShaderBinary) const +{ + // Find source debug info for the shader DebugName in the shader database. + std::vector shaderBinary; + if (!m_shaderDatabase.FindShaderBinaryWithDebugData(shaderDebugName, shaderBinary)) + { + // Early exit, nothing found. No need to call setShaderBinary. + return; + } + + // Let the GPU crash dump decoder know about the shader debug data that was + // found. + setShaderBinary(shaderBinary.data(), uint32_t(shaderBinary.size())); +} + +// Static callback wrapper for OnCrashDump +void GpuCrashTracker::GpuCrashDumpCallback( + const void* pGpuCrashDump, + const uint32_t gpuCrashDumpSize, + void* pUserData) +{ + GpuCrashTracker* pGpuCrashTracker = reinterpret_cast(pUserData); + pGpuCrashTracker->OnCrashDump(pGpuCrashDump, gpuCrashDumpSize); +} + +// Static callback wrapper for OnShaderDebugInfo +void GpuCrashTracker::ShaderDebugInfoCallback( + const void* pShaderDebugInfo, + const uint32_t shaderDebugInfoSize, + void* pUserData) +{ + GpuCrashTracker* pGpuCrashTracker = reinterpret_cast(pUserData); + pGpuCrashTracker->OnShaderDebugInfo(pShaderDebugInfo, shaderDebugInfoSize); +} + +// Static callback wrapper for OnDescription +void GpuCrashTracker::CrashDumpDescriptionCallback( + PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription, + void* pUserData) +{ + GpuCrashTracker* pGpuCrashTracker = reinterpret_cast(pUserData); + pGpuCrashTracker->OnDescription(addDescription); +} + +// Static callback wrapper for OnShaderDebugInfoLookup +void GpuCrashTracker::ShaderDebugInfoLookupCallback( + const GFSDK_Aftermath_ShaderDebugInfoIdentifier* pIdentifier, + PFN_GFSDK_Aftermath_SetData setShaderDebugInfo, + void* pUserData) +{ + GpuCrashTracker* pGpuCrashTracker = reinterpret_cast(pUserData); + pGpuCrashTracker->OnShaderDebugInfoLookup(*pIdentifier, setShaderDebugInfo); +} + +// Static callback wrapper for OnShaderLookup +void GpuCrashTracker::ShaderLookupCallback( + const GFSDK_Aftermath_ShaderHash* pShaderHash, + PFN_GFSDK_Aftermath_SetData setShaderBinary, + void* pUserData) +{ + GpuCrashTracker* pGpuCrashTracker = reinterpret_cast(pUserData); + pGpuCrashTracker->OnShaderLookup(*pShaderHash, setShaderBinary); +} + +// Static callback wrapper for OnShaderSourceDebugInfoLookup +void GpuCrashTracker::ShaderSourceDebugInfoLookupCallback( + const GFSDK_Aftermath_ShaderDebugName* pShaderDebugName, + PFN_GFSDK_Aftermath_SetData setShaderBinary, + void* pUserData) +{ + GpuCrashTracker* pGpuCrashTracker = reinterpret_cast(pUserData); + pGpuCrashTracker->OnShaderSourceDebugInfoLookup(*pShaderDebugName, setShaderBinary); +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathGpuCrashTracker.h b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathGpuCrashTracker.h new file mode 100644 index 00000000..289d380d --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathGpuCrashTracker.h @@ -0,0 +1,159 @@ +//********************************************************* +// +// Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//********************************************************* + +#pragma once + +#include +#include + +#include "NsightAftermathHelpers.h" +#include "NsightAftermathShaderDatabase.h" + +//********************************************************* +// Implements GPU crash dump tracking using the Nsight +// Aftermath API. +// +class GpuCrashTracker +{ +public: + GpuCrashTracker(); + ~GpuCrashTracker(); + + // Initialize the GPU crash dump tracker. + void Initialize(); + +private: + + //********************************************************* + // Callback handlers for GPU crash dumps and related data. + // + + // Handler for GPU crash dump callbacks. + void OnCrashDump(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize); + + // Handler for shader debug information callbacks. + void OnShaderDebugInfo(const void* pShaderDebugInfo, const uint32_t shaderDebugInfoSize); + + // Handler for GPU crash dump description callbacks. + void OnDescription(PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription); + + //********************************************************* + // Helpers for writing a GPU crash dump and debug information + // data to files. + // + + // Helper for writing a GPU crash dump to a file. + void WriteGpuCrashDumpToFile(const void* pGpuCrashDump, const uint32_t gpuCrashDumpSize); + + // Helper for writing shader debug information to a file + void WriteShaderDebugInformationToFile( + GFSDK_Aftermath_ShaderDebugInfoIdentifier identifier, + const void* pShaderDebugInfo, + const uint32_t shaderDebugInfoSize); + + //********************************************************* + // Helpers for decoding GPU crash dump to JSON. + // + + // Handler for shader debug info lookup callbacks. + void OnShaderDebugInfoLookup( + const GFSDK_Aftermath_ShaderDebugInfoIdentifier& identifier, + PFN_GFSDK_Aftermath_SetData setShaderDebugInfo) const; + + // Handler for shader lookup callbacks. + void OnShaderLookup( + const GFSDK_Aftermath_ShaderHash& shaderHash, + PFN_GFSDK_Aftermath_SetData setShaderBinary) const; + + // Handler for shader instructions lookup callbacks. + void OnShaderInstructionsLookup( + const GFSDK_Aftermath_ShaderInstructionsHash& shaderInstructionsHash, + PFN_GFSDK_Aftermath_SetData setShaderBinary) const; + + // Handler for shader source debug info lookup callbacks. + void OnShaderSourceDebugInfoLookup( + const GFSDK_Aftermath_ShaderDebugName& shaderDebugName, + PFN_GFSDK_Aftermath_SetData setShaderBinary) const; + + //********************************************************* + // Static callback wrappers. + // + + // GPU crash dump callback. + static void GpuCrashDumpCallback( + const void* pGpuCrashDump, + const uint32_t gpuCrashDumpSize, + void* pUserData); + + // Shader debug information callback. + static void ShaderDebugInfoCallback( + const void* pShaderDebugInfo, + const uint32_t shaderDebugInfoSize, + void* pUserData); + + // GPU crash dump description callback. + static void CrashDumpDescriptionCallback( + PFN_GFSDK_Aftermath_AddGpuCrashDumpDescription addDescription, + void* pUserData); + + // Shader debug information lookup callback. + static void ShaderDebugInfoLookupCallback( + const GFSDK_Aftermath_ShaderDebugInfoIdentifier* pIdentifier, + PFN_GFSDK_Aftermath_SetData setShaderDebugInfo, + void* pUserData); + + // Shader lookup callback. + static void ShaderLookupCallback( + const GFSDK_Aftermath_ShaderHash* pShaderHash, + PFN_GFSDK_Aftermath_SetData setShaderBinary, + void* pUserData); + + // Shader instructions lookup callback. + static void ShaderInstructionsLookupCallback( + const GFSDK_Aftermath_ShaderInstructionsHash* pShaderInstructionsHash, + PFN_GFSDK_Aftermath_SetData setShaderBinary, + void* pUserData); + + // Shader source debug info lookup callback. + static void ShaderSourceDebugInfoLookupCallback( + const GFSDK_Aftermath_ShaderDebugName* pShaderDebugName, + PFN_GFSDK_Aftermath_SetData setShaderBinary, + void* pUserData); + + //********************************************************* + // GPU crash tracker state. + // + + // Is the GPU crash dump tracker initialized? + bool m_initialized; + + // For thread-safe access of GPU crash tracker state. + mutable std::mutex m_mutex; + + // List of Shader Debug Information by ShaderDebugInfoIdentifier. + std::map> m_shaderDebugInfo; + + // The mock shader database. + ShaderDatabase m_shaderDatabase; +}; diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathHelpers.h b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathHelpers.h new file mode 100644 index 00000000..1e75fa09 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathHelpers.h @@ -0,0 +1,142 @@ +//********************************************************* +// +// Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//********************************************************* + +#pragma once + +#include +#include +#include + +#include +#include "GFSDK_Aftermath.h" +#include "GFSDK_Aftermath_GpuCrashDump.h" +#include "GFSDK_Aftermath_GpuCrashDumpDecoding.h" + +//********************************************************* +// Some std::to_string overloads for some Nsight Aftermath +// API types. +// + +namespace std +{ + template + inline std::string to_hex_string(T n) + { + std::stringstream stream; + stream << std::setfill('0') << std::setw(2 * sizeof(T)) << std::hex << n; + return stream.str(); + } + + inline std::string to_string(GFSDK_Aftermath_Result result) + { + return std::string("0x") + to_hex_string(static_cast(result)); + } + + inline std::string to_string(const GFSDK_Aftermath_ShaderDebugInfoIdentifier& identifier) + { + return to_hex_string(identifier.id[0]) + "-" + to_hex_string(identifier.id[1]); + } + + inline std::string to_string(const GFSDK_Aftermath_ShaderHash& hash) + { + return to_hex_string(hash.hash); + } + + inline std::string to_string(const GFSDK_Aftermath_ShaderInstructionsHash& hash) + { + return to_hex_string(hash.hash) + "-" + to_hex_string(hash.hash); + } +} // namespace std + +//********************************************************* +// Helper for comparing shader hashes and debug info identifier. +// + +// Helper for comparing GFSDK_Aftermath_ShaderDebugInfoIdentifier. +inline bool operator<(const GFSDK_Aftermath_ShaderDebugInfoIdentifier& lhs, const GFSDK_Aftermath_ShaderDebugInfoIdentifier& rhs) +{ + if (lhs.id[0] == rhs.id[0]) + { + return lhs.id[1] < rhs.id[1]; + } + return lhs.id[0] < rhs.id[0]; +} + +// Helper for comparing GFSDK_Aftermath_ShaderHash. +inline bool operator<(const GFSDK_Aftermath_ShaderHash& lhs, const GFSDK_Aftermath_ShaderHash& rhs) +{ + return lhs.hash < rhs.hash; +} + +// Helper for comparing GFSDK_Aftermath_ShaderInstructionsHash. +inline bool operator<(const GFSDK_Aftermath_ShaderInstructionsHash& lhs, const GFSDK_Aftermath_ShaderInstructionsHash& rhs) +{ + return lhs.hash < rhs.hash; +} + +// Helper for comparing GFSDK_Aftermath_ShaderDebugName. +inline bool operator<(const GFSDK_Aftermath_ShaderDebugName& lhs, const GFSDK_Aftermath_ShaderDebugName& rhs) +{ + return strncmp(lhs.name, rhs.name, sizeof(lhs.name)) < 0; +} + +//********************************************************* +// Helper for checking Nsight Aftermath failures. +// + +inline std::string AftermathErrorMessage(GFSDK_Aftermath_Result result) +{ + switch (result) + { + case GFSDK_Aftermath_Result_FAIL_DriverVersionNotSupported: + return "Unsupported driver version - requires a recent NVIDIA R445 display driver or newer."; + default: + return "Aftermath Error 0x" + std::to_hex_string(result); + } +} + +// Helper macro for checking Nsight Aftermath results and throwing exception +// in case of a failure. +#ifdef _WIN32 +#define AFTERMATH_CHECK_ERROR(FC) \ +[&]() { \ + GFSDK_Aftermath_Result _result = FC; \ + if (!GFSDK_Aftermath_SUCCEED(_result)) \ + { \ + MessageBoxA(0, AftermathErrorMessage(_result).c_str(), "Aftermath Error", MB_OK); \ + exit(1); \ + } \ +}() +#else +#define AFTERMATH_CHECK_ERROR(FC) \ +[&]() { \ + GFSDK_Aftermath_Result _result = FC; \ + if (!GFSDK_Aftermath_SUCCEED(_result)) \ + { \ + printf("%s\n", AftermathErrorMessage(_result).c_str()); \ + fflush(stdout); \ + exit(1); \ + } \ +}() +#endif diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathShaderDatabase.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathShaderDatabase.cpp new file mode 100644 index 00000000..675e5784 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathShaderDatabase.cpp @@ -0,0 +1,148 @@ +//********************************************************* +// +// Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//********************************************************* + +#include "sepch.h" +#include +#include + +#include "NsightAftermathShaderDatabase.h" + +//********************************************************* +// ShaderDatabase implementation +//********************************************************* + +ShaderDatabase::ShaderDatabase() + : m_shaderBinaries() + , m_shaderBinariesWithDebugInfo() +{ + // Add shader binaries to database + AddShaderBinary("cube.vert.spirv"); + AddShaderBinary("cube.frag.spirv"); + + // Add the not stripped shader binaries to the database, too. + AddShaderBinaryWithDebugInfo("cube.vert.spirv", "cube.vert.full.spirv"); + AddShaderBinaryWithDebugInfo("cube.frag.spirv", "cube.frag.full.spirv"); +} + +ShaderDatabase::~ShaderDatabase() +{ +} + +bool ShaderDatabase::ReadFile(const char* filename, std::vector& data) +{ + std::ifstream fs(filename, std::ios::in | std::ios::binary); + if (!fs) + { + return false; + } + + fs.seekg(0, std::ios::end); + data.resize(fs.tellg()); + fs.seekg(0, std::ios::beg); + fs.read(reinterpret_cast(data.data()), data.size()); + fs.close(); + + return true; +} + +void ShaderDatabase::AddShaderBinary(const char* shaderFilePath) +{ + // Read the shader binary code from the file + std::vector data; + if (!ReadFile(shaderFilePath, data)) + { + return; + } + + // Create shader hash for the shader + const GFSDK_Aftermath_SpirvCode shader{ data.data(), uint32_t(data.size()) }; + GFSDK_Aftermath_ShaderHash shaderHash; + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GetShaderHashSpirv( + GFSDK_Aftermath_Version_API, + &shader, + &shaderHash)); + + // Store the data for shader mapping when decoding GPU crash dumps. + // cf. FindShaderBinary() + m_shaderBinaries[shaderHash].swap(data); +} + +void ShaderDatabase::AddShaderBinaryWithDebugInfo(const char* strippedShaderFilePath, const char* shaderFilePath) +{ + // Read the shader debug data from the file + std::vector data; + if (!ReadFile(shaderFilePath, data)) + { + return; + } + std::vector strippedData; + if (!ReadFile(strippedShaderFilePath, strippedData)) + { + return; + } + + // Generate shader debug name. + GFSDK_Aftermath_ShaderDebugName debugName; + const GFSDK_Aftermath_SpirvCode shader{ data.data(), uint32_t(data.size()) }; + const GFSDK_Aftermath_SpirvCode strippedShader{ strippedData.data(), uint32_t(strippedData.size()) }; + AFTERMATH_CHECK_ERROR(GFSDK_Aftermath_GetShaderDebugNameSpirv( + GFSDK_Aftermath_Version_API, + &shader, + &strippedShader, + &debugName)); + + // Store the data for shader instruction address mapping when decoding GPU crash dumps. + // cf. FindShaderBinaryWithDebugData() + m_shaderBinariesWithDebugInfo[debugName].swap(data); +} + +// Find a shader binary by shader hash. +bool ShaderDatabase::FindShaderBinary(const GFSDK_Aftermath_ShaderHash& shaderHash, std::vector& shader) const +{ + // Find shader binary data for the shader hash + auto i_shader = m_shaderBinaries.find(shaderHash); + if (i_shader == m_shaderBinaries.end()) + { + // Nothing found. + return false; + } + + shader = i_shader->second; + return true; +} + +// Find a shader binary with debug information by shader debug name. +bool ShaderDatabase::FindShaderBinaryWithDebugData(const GFSDK_Aftermath_ShaderDebugName& shaderDebugName, std::vector& shader) const +{ + // Find shader binary for the shader debug name. + auto i_shader = m_shaderBinariesWithDebugInfo.find(shaderDebugName); + if (i_shader == m_shaderBinariesWithDebugInfo.end()) + { + // Nothing found. + return false; + } + + shader = i_shader->second; + return true; +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathShaderDatabase.h b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathShaderDatabase.h new file mode 100644 index 00000000..0402cdb8 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/Debug/NsightAftermathShaderDatabase.h @@ -0,0 +1,64 @@ +//********************************************************* +// +// Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +//********************************************************* + +#pragma once + +#include +#include +#include + +#include "NsightAftermathHelpers.h" + +//********************************************************* +// Implements a very simple shader database to help demonstrate +// how to use the Nsight Aftermath GPU crash dump decoder API. +// +// In a real world scenario this would be part of an offline +// analysis tool. This is for demonstration purposes only! +// +class ShaderDatabase +{ +public: + ShaderDatabase(); + ~ShaderDatabase(); + + // Find a shader bytecode binary by shader hash. + bool FindShaderBinary(const GFSDK_Aftermath_ShaderHash& shaderHash, std::vector& shader) const; + + // Find a source shader debug info by shader debug name generated by the DXC compiler. + bool FindShaderBinaryWithDebugData(const GFSDK_Aftermath_ShaderDebugName& shaderDebugName, std::vector& shader) const; + +private: + + void AddShaderBinary(const char* shaderFilePath); + void AddShaderBinaryWithDebugInfo(const char* strippedShaderFilePath, const char* shaderFilePath); + + static bool ReadFile(const char* filename, std::vector& data); + + // List of shader binaries by ShaderHash. + std::map> m_shaderBinaries; + + // List of available shader binaries with source debug information by ShaderDebugName. + std::map> m_shaderBinariesWithDebugInfo; +}; diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/DescriptorSetManager.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/DescriptorSetManager.cpp new file mode 100644 index 00000000..c18cd0a7 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/DescriptorSetManager.cpp @@ -0,0 +1,1098 @@ +#include "sepch.h" +#include "DescriptorSetManager.h" + +#include "StarEngine/Renderer/Renderer.h" + +#include "VulkanAPI.h" + +#include "StarEngine/Debug/Profiler.h" + +namespace StarEngine { + + namespace Utils { + + inline RenderResourceType GetDefaultResourceType(RenderInputType inputType) + { + switch (inputType) + { + case RenderInputType::ImageSampler: return RenderResourceType::Sampler; + case RenderInputType::ImageSampler2D: return RenderResourceType::Texture2D; + case RenderInputType::ImageSampler3D: return RenderResourceType::TextureCube; + case RenderInputType::StorageImage2D: return RenderResourceType::Image2D; + case RenderInputType::UniformBuffer: return RenderResourceType::UniformBuffer; + case RenderInputType::StorageBuffer: return RenderResourceType::StorageBuffer; + } + + SE_CORE_ASSERT(false); + return RenderResourceType::None; + } + + } + + DescriptorSetManager::DescriptorSetManager(const DescriptorSetManagerSpecification& specification) + : m_Specification(specification) + { + Init(); + } + + DescriptorSetManager::DescriptorSetManager(const DescriptorSetManager& other) + : m_Specification(other.m_Specification) + { + Init(); + InputResources = other.InputResources; + Bake(); + } + + DescriptorSetManager DescriptorSetManager::Copy(const DescriptorSetManager& other) + { + DescriptorSetManager result(other); + return result; + } + + void DescriptorSetManager::Init() + { + const auto& shaderDescriptorSets = m_Specification.Shader->GetShaderDescriptorSets(); + uint32_t framesInFlight = Renderer::GetConfig().FramesInFlight; + m_BindingSetHandles.resize(framesInFlight); + + for (uint32_t set = m_Specification.StartSet; set <= m_Specification.EndSet; set++) + { + if (set >= shaderDescriptorSets.size()) + break; + + const auto& shaderDescriptor = shaderDescriptorSets[set]; + for (auto&& [bname, inputDecl] : shaderDescriptor.InputDeclarations) + { + // NOTE(Emily): This is a hack to fix a bad input decl name + // Coming from somewhere. + const char* broken = strrchr(bname.c_str(), '.'); + std::string name = broken ? broken + 1 : bname; + + InputDeclarations[name] = inputDecl; + + uint32_t binding = inputDecl.Binding; + + // Insert default resources (useful for materials) + if (m_Specification.DefaultResources || true) + { + // Create RenderPassInput + RenderPassInput& input = InputResources[set][binding]; + input.Input.resize(inputDecl.Count); + input.Type = Utils::GetDefaultResourceType(inputDecl.Type); + + // Set default textures and samplers + if (inputDecl.Type == RenderInputType::ImageSampler) + { + for (size_t i = 0; i < input.Input.size(); i++) + input.Input[i] = Renderer::GetDefaultSampler(); + } + if (inputDecl.Type == RenderInputType::ImageSampler2D) + { + for (size_t i = 0; i < input.Input.size(); i++) + input.Input[i] = Renderer::GetWhiteTexture(); + } + else if (inputDecl.Type == RenderInputType::ImageSampler3D) + { + for (size_t i = 0; i < input.Input.size(); i++) + input.Input[i] = Renderer::GetBlackCubeTexture(); + } + } + + for (uint32_t frameIndex = 0; frameIndex < framesInFlight; frameIndex++) + m_BindingSetHandles[frameIndex][set][binding].resize(inputDecl.Count); + + } + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref uniformBufferSet) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(uniformBufferSet); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref uniformBuffer) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(uniformBuffer); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref storageBufferSet) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(storageBufferSet); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref storageBuffer) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(storageBuffer); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref texture, uint32_t index) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(texture, index); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref textureCube) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(textureCube); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref image) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(image); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref image) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + InputResources.at(decl->Set).at(decl->Binding).Set(image); + else + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + + void DescriptorSetManager::SetInput(std::string_view name, Ref sampler) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + InputResources.at(decl->Set).at(decl->Binding).Set(sampler); + m_State = State::Pending; + } + else + { + SE_CORE_WARN_TAG("Renderer", "[RenderPass ({})] Input {} not found", m_Specification.DebugName, name); + } + } + + bool DescriptorSetManager::IsInvalidated(uint32_t set, uint32_t binding) const + { + if (InvalidatedInputResources.find(set) != InvalidatedInputResources.end()) + { + const auto& resources = InvalidatedInputResources.at(set); + return resources.find(binding) != resources.end(); + } + + return false; + } + + std::set DescriptorSetManager::HasBufferSets() const + { + // Find all descriptor sets that have either UniformBufferSet or StorageBufferSet descriptors + std::set sets; + + for (const auto& [set, resources] : InputResources) + { + for (const auto& [binding, input] : resources) + { + if (input.Type == RenderResourceType::UniformBufferSet || input.Type == RenderResourceType::StorageBufferSet) + { + sets.insert(set); + break; + } + } + } + return sets; + } + + + bool DescriptorSetManager::Validate() + { + // Go through pipeline requirements to make sure we have all required resource + const auto& shaderDescriptorSets = m_Specification.Shader->GetShaderDescriptorSets(); + + // Nothing to validate, pipeline only contains material inputs + //if (shaderDescriptorSets.size() < 2) + // return true; + + for (uint32_t set = m_Specification.StartSet; set <= m_Specification.EndSet; set++) + { + if (set >= shaderDescriptorSets.size()) + break; + + // No descriptors in this set + if (!shaderDescriptorSets[set]) + continue; + + if (InputResources.find(set) == InputResources.end()) + { + SE_CORE_ERROR_TAG("Renderer", "[RenderPass ({})] No input resources for Set {}", m_Specification.DebugName, set); + return false; + } + + const auto& setInputResources = InputResources.at(set); + + const auto& shaderDescriptor = shaderDescriptorSets[set]; + for (auto&& [name, inputDecl] : shaderDescriptor.InputDeclarations) + { + uint32_t binding = inputDecl.Binding; + if (setInputResources.find(binding) == setInputResources.end()) + { + SE_CORE_ERROR_TAG("Renderer", "[RenderPass ({})] No input resource for {}.{}", m_Specification.DebugName, set, binding); + SE_CORE_ERROR_TAG("Renderer", "[RenderPass ({})] Required resource is {} ({})", m_Specification.DebugName, name, (int)inputDecl.Type); + return false; + } + + const auto& resource = setInputResources.at(binding); + if (!IsCompatibleInput(resource.Type, inputDecl.Type)) + { + SE_CORE_ERROR_TAG("Renderer", "[RenderPass ({})] Required resource is wrong type! {} but needs {}", m_Specification.DebugName, (uint16_t)resource.Type, (int)inputDecl.Type); + return false; + } + + if (resource.Type != RenderResourceType::Image2D && resource.Input[0] == nullptr) + { + SE_CORE_ERROR_TAG("Renderer", "[RenderPass ({})] Resource is null! {} ({}.{})", m_Specification.DebugName, name, set, binding); + return false; + } + } + } + + // All resources present + return true; + } + + // TODO(Yan): revisit resources not existing at this time, since we now (mostly) create them immediately + void DescriptorSetManager::Bake() + { + // Make sure all resources are present and we can properly bake + if (!Validate()) + { + SE_CORE_ERROR_TAG("Renderer", "[RenderPass] Bake - Validate failed! {}", m_Specification.DebugName); + return; + } + + // If valid, we can create descriptor sets + nvrhi::DeviceHandle device = Application::GetGraphicsDevice(); + + auto bufferSets = HasBufferSets(); + bool perFrameInFlight = !bufferSets.empty(); + perFrameInFlight = true; // always + uint32_t descriptorSetCount = Renderer::GetConfig().FramesInFlight; + if (!perFrameInFlight) + descriptorSetCount = 1; + + m_BindingSets.resize(descriptorSetCount); + for (auto& set : m_BindingSets) + set = {}; + + // for (auto& set : m_BindingSetHandles) + // set.clear(); + + for (const auto& [set, setData] : InputResources) + { + uint32_t descriptorCountInSet = bufferSets.find(set) != bufferSets.end() ? descriptorSetCount : 1; + for (uint32_t frameIndex = 0; frameIndex < descriptorSetCount; frameIndex++) + { + nvrhi::BindingLayoutHandle bindingLayout = m_Specification.Shader->GetDescriptorSetLayout(set); + + nvrhi::BindingSetDesc bindingSetDesc; + + if (m_BindingSets[frameIndex].size() <= set) + m_BindingSets[frameIndex].resize(set + 1); + + auto& bindingSetHandleMap = m_BindingSetHandles[frameIndex].at(set); + std::vector> imageInfoStorage; + uint32_t imageInfoStorageIndex = 0; + + for (const auto& [binding, input] : setData) + { + auto& storedHandles = bindingSetHandleMap.at(binding); + storedHandles.resize(input.Input.size()); + + // VkWriteDescriptorSet& writeDescriptor = storedWriteDescriptor.WriteDescriptorSet; + // writeDescriptor.dstSet = descriptorSet; + + switch (input.Type) + { + case RenderResourceType::UniformBuffer: + { + Ref buffer = input.Input[0].As(); + nvrhi::BufferHandle handle = buffer->GetHandle(); + bindingSetDesc.bindings.push_back(nvrhi::BindingSetItem::ConstantBuffer(binding, handle)); + storedHandles[0] = handle; + + // Defer if resource doesn't exist + if (buffer->GetHandle() == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + case RenderResourceType::UniformBufferSet: + { + Ref buffer = input.Input[0].As(); + nvrhi::BufferHandle handle = buffer->Get(frameIndex)->GetHandle(); + bindingSetDesc.bindings.push_back(nvrhi::BindingSetItem::ConstantBuffer(binding, handle)); + storedHandles[0] = handle; + + break; + } + case RenderResourceType::StorageBuffer: + { + Ref buffer = input.Input[0].As(); + nvrhi::BufferHandle handle = buffer->GetHandle(); + bindingSetDesc.bindings.push_back(nvrhi::BindingSetItem::RawBuffer_UAV(binding, handle)); + storedHandles[0] = handle; + + break; + } + case RenderResourceType::StorageBufferSet: + { + Ref buffer = input.Input[0].As(); + nvrhi::BufferHandle handle = buffer->Get(frameIndex)->GetHandle(); + bindingSetDesc.bindings.push_back(nvrhi::BindingSetItem::RawBuffer_UAV(binding, handle)); + storedHandles[0] = handle; + + break; + } + case RenderResourceType::Texture2D: + { + for (size_t i = 0; i < input.Input.size(); i++) + { + Ref texture = input.Input[i].As(); + + nvrhi::TextureHandle handle = texture->GetHandle(); + nvrhi::BindingSetItem bindingSetItem = nvrhi::BindingSetItem::Texture_SRV(binding, handle); + bindingSetItem.arrayElement = (uint32_t)i; + bindingSetDesc.bindings.push_back(bindingSetItem); + + storedHandles[i] = handle; + } + + break; + } + case RenderResourceType::TextureCube: + { + Ref texture = input.Input[0].As(); + nvrhi::TextureHandle handle = texture->GetHandle(); + bindingSetDesc.bindings.push_back(nvrhi::BindingSetItem::Texture_SRV(binding, handle)); + storedHandles[0] = handle; + + break; + } + case RenderResourceType::Image2D: + { + Ref image = input.Input[0].As(); + // Defer if resource doesn't exist + if (image == nullptr) + { + InvalidatedInputResources[set][binding] = input; + break; + } + + nvrhi::TextureHandle handle = ((ImageInfo*)image->GetDescriptorInfo())->ImageHandle; + bindingSetDesc.bindings.push_back(nvrhi::BindingSetItem::Texture_SRV(binding, handle)); + storedHandles[0] = handle; + + break; + } + case RenderResourceType::Sampler: + { + Ref sampler = input.Input[0].As(); + // Defer if resource doesn't exist + if (sampler == nullptr) + { + InvalidatedInputResources[set][binding] = input; + break; + } + + nvrhi::SamplerHandle handle = ((Sampler*)sampler->GetDescriptorInfo())->GetHandle(); + bindingSetDesc.bindings.push_back(nvrhi::BindingSetItem::Sampler(binding, handle)); + storedHandles[0] = handle; + + break; + } + } + } + + if (!bindingSetDesc.bindings.empty()) + { + m_BindingSets[frameIndex][set] = device->createBindingSet(bindingSetDesc, bindingLayout); + } + } + } + +#if 1 + for (uint32_t frameIndex = 0; frameIndex < descriptorSetCount; frameIndex++) + { + if (!m_BindingSets[frameIndex].empty() && m_BindingSets[frameIndex][0] == nullptr) + { + nvrhi::BindingLayoutHandle bindingLayout = m_Specification.Shader->GetDescriptorSetLayout(0); + + nvrhi::BindingSetDesc bindingSetDesc; + m_BindingSets[frameIndex][0] = device->createBindingSet(bindingSetDesc, bindingLayout); + } + } +#endif + + +#if TODO + // Create Descriptor Pool + VkDescriptorPoolSize poolSizes[] = + { + { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }, + { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 }, + { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 } + }; + + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + poolInfo.maxSets = 10 * 3; // frames in flight should partially determine this + poolInfo.poolSizeCount = 10; + poolInfo.pPoolSizes = poolSizes; + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &poolInfo, nullptr, &m_DescriptorPool)); + + auto bufferSets = HasBufferSets(); + bool perFrameInFlight = !bufferSets.empty(); + perFrameInFlight = true; // always + uint32_t descriptorSetCount = Renderer::GetConfig().FramesInFlight; + if (!perFrameInFlight) + descriptorSetCount = 1; + + if (m_DescriptorSets.size() < 1) + { + for (uint32_t i = 0; i < descriptorSetCount; i++) + m_DescriptorSets.emplace_back(); + } + + for (auto& descriptorSet : m_DescriptorSets) + descriptorSet.clear(); + + for (const auto& [set, setData] : InputResources) + { + uint32_t descriptorCountInSet = bufferSets.find(set) != bufferSets.end() ? descriptorSetCount : 1; + for (uint32_t frameIndex = 0; frameIndex < descriptorSetCount; frameIndex++) + { + nvrhi::BindingLayoutHandle dsl = m_Specification.Shader->GetDescriptorSetLayout(set); + VkDescriptorSetAllocateInfo descriptorSetAllocInfo = Vulkan::DescriptorSetAllocInfo(&dsl); + descriptorSetAllocInfo.descriptorPool = m_DescriptorPool; + VkDescriptorSet descriptorSet = nullptr; + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &descriptorSetAllocInfo, &descriptorSet)); + + m_DescriptorSets[frameIndex].emplace_back(descriptorSet); + + auto& writeDescriptorMap = WriteDescriptorMap[frameIndex].at(set); + std::vector> imageInfoStorage; + uint32_t imageInfoStorageIndex = 0; + + for (const auto& [binding, input] : setData) + { + auto& storedWriteDescriptor = writeDescriptorMap.at(binding); + + VkWriteDescriptorSet& writeDescriptor = storedWriteDescriptor.WriteDescriptorSet; + writeDescriptor.dstSet = descriptorSet; + + switch (input.Type) + { + case RenderResourceType::UniformBuffer: + { + Ref buffer = input.Input[0].As(); + writeDescriptor.pBufferInfo = &buffer->GetDescriptorBufferInfo(); + storedWriteDescriptor.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + + // Defer if resource doesn't exist + if (writeDescriptor.pBufferInfo->buffer == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + case RenderResourceType::UniformBufferSet: + { + Ref buffer = input.Input[0].As(); + // TODO: replace 0 with current frame in flight (i.e. create bindings for all frames) + writeDescriptor.pBufferInfo = &buffer->Get(frameIndex).As()->GetDescriptorBufferInfo(); + storedWriteDescriptor.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + + // Defer if resource doesn't exist + if (writeDescriptor.pBufferInfo->buffer == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + case RenderResourceType::StorageBuffer: + { + Ref buffer = input.Input[0].As(); + writeDescriptor.pBufferInfo = &buffer->GetDescriptorBufferInfo(); + storedWriteDescriptor.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + + // Defer if resource doesn't exist + if (writeDescriptor.pBufferInfo->buffer == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + case RenderResourceType::StorageBufferSet: + { + Ref buffer = input.Input[0].As(); + // TODO: replace 0 with current frame in flight (i.e. create bindings for all frames) + writeDescriptor.pBufferInfo = &buffer->Get(frameIndex).As()->GetDescriptorBufferInfo(); + storedWriteDescriptor.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + + // Defer if resource doesn't exist + if (writeDescriptor.pBufferInfo->buffer == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + case RenderResourceType::Texture2D: + { + if (input.Input.size() > 1) + { + imageInfoStorage.emplace_back(input.Input.size()); + for (size_t i = 0; i < input.Input.size(); i++) + { + Ref texture = input.Input[i].As(); + imageInfoStorage[imageInfoStorageIndex][i] = texture->GetDescriptorInfoVulkan(); + + } + writeDescriptor.pImageInfo = imageInfoStorage[imageInfoStorageIndex].data(); + imageInfoStorageIndex++; + } + else + { + Ref texture = input.Input[0].As(); + writeDescriptor.pImageInfo = &texture->GetDescriptorInfoVulkan(); + } + storedWriteDescriptor.ResourceHandles[0] = writeDescriptor.pImageInfo->imageView; + + // Defer if resource doesn't exist + if (writeDescriptor.pImageInfo->imageView == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + case RenderResourceType::TextureCube: + { + Ref texture = input.Input[0].As(); + writeDescriptor.pImageInfo = &texture->GetDescriptorInfoVulkan(); + storedWriteDescriptor.ResourceHandles[0] = writeDescriptor.pImageInfo->imageView; + + // Defer if resource doesn't exist + if (writeDescriptor.pImageInfo->imageView == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + case RenderResourceType::Image2D: + { + Ref image = input.Input[0].As(); + // Defer if resource doesn't exist + if (image == nullptr) + { + InvalidatedInputResources[set][binding] = input; + break; + } + + writeDescriptor.pImageInfo = (VkDescriptorImageInfo*)image->GetDescriptorInfo(); + storedWriteDescriptor.ResourceHandles[0] = writeDescriptor.pImageInfo->imageView; + + // Defer if resource doesn't exist + if (writeDescriptor.pImageInfo->imageView == nullptr) + InvalidatedInputResources[set][binding] = input; + + break; + } + } + } + + std::vector writeDescriptors; + for (auto&& [binding, writeDescriptor] : writeDescriptorMap) + { + // Include if valid, otherwise defer (these will be resolved if possible at Prepare stage) + if (!IsInvalidated(set, binding)) + writeDescriptors.emplace_back(writeDescriptor.WriteDescriptorSet); + } + + if (!writeDescriptors.empty()) + { + HZ_CORE_INFO_TAG("Renderer", "Render pass update {} descriptors in set {}", writeDescriptors.size(), set); + vkUpdateDescriptorSets(device, (uint32_t)writeDescriptors.size(), writeDescriptors.data(), 0, nullptr); + } + } + } +#endif + } + + void DescriptorSetManager::InvalidateAndUpdate() + { + SE_PROFILE_FUNCTION("DescriptorSetManager::InvalidateAndUpdate"); + SE_SCOPE_PERF("DescriptorSetManager::InvalidateAndUpdate"); + + if (m_State == State::Ready) + return; + + uint32_t currentFrameIndex = Renderer::RT_GetCurrentFrameIndex(); + + // Check for invalidated resources + for (const auto& [set, inputs] : InputResources) + { + for (const auto& [binding, input] : inputs) + { + const auto& bindingSetHandleArray = m_BindingSetHandles[currentFrameIndex].at(set).at(binding); + const auto& bindingSetHandle = bindingSetHandleArray[0]; + + switch (input.Type) + { + case RenderResourceType::UniformBuffer: + { + nvrhi::BufferHandle handle = input.Input[0].As()->GetHandle(); + if (handle != bindingSetHandle) + { + InvalidatedInputResources[set][binding] = input; + break; + } + break; + } + case RenderResourceType::UniformBufferSet: + { + nvrhi::BufferHandle handle = input.Input[0].As()->Get(currentFrameIndex)->GetHandle(); + if (handle != bindingSetHandle) + { + InvalidatedInputResources[set][binding] = input; + break; + } + break; + } + case RenderResourceType::StorageBuffer: + { + nvrhi::BufferHandle handle = input.Input[0].As()->GetHandle(); + if (handle != bindingSetHandle) + { + InvalidatedInputResources[set][binding] = input; + break; + } + break; + } + case RenderResourceType::StorageBufferSet: + { + nvrhi::BufferHandle handle = input.Input[0].As()->Get(currentFrameIndex)->GetHandle(); + if (handle != bindingSetHandle) + { + InvalidatedInputResources[set][binding] = input; + break; + } + break; + } + case RenderResourceType::Texture2D: + { + for (size_t i = 0; i < input.Input.size(); i++) + { + Ref texture = input.Input[i].As(); + if (texture == nullptr) + { + texture = Renderer::GetWhiteTexture(); // TODO(Yan): error texture + SE_CORE_VERIFY(false); + } + + if (texture->GetHandle() != bindingSetHandleArray[i]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + case RenderResourceType::TextureCube: + { + nvrhi::TextureHandle handle = input.Input[0].As()->GetHandle(); + if (handle != bindingSetHandle) + { + InvalidatedInputResources[set][binding] = input; + break; + } + break; + } + case RenderResourceType::Image2D: + { + Ref image = input.Input[0].As(); + nvrhi::TextureHandle handle = ((ImageInfo*)image->GetDescriptorInfo())->ImageHandle; + if (handle != bindingSetHandle) + { + InvalidatedInputResources[set][binding] = input; + break; + } + break; + } + case RenderResourceType::Sampler: + { + Ref image = input.Input[0].As(); + nvrhi::SamplerHandle handle = ((Sampler*)image->GetDescriptorInfo())->GetHandle(); + if (handle != bindingSetHandle) + { + InvalidatedInputResources[set][binding] = input; + break; + } + break; + } + } + } + } + + if (!InvalidatedInputResources.empty()) + { + SE_CORE_INFO_TAG("Renderer", "DescriptorSetManager::InvalidateAndUpdate ({}) - updating {} descriptors (frameIndex={})", m_Specification.DebugName, InvalidatedInputResources.size(), currentFrameIndex); + Bake(); + } + + if (!m_Specification.IsDynamic) + m_State = State::Ready; + +#if TODO + HZ_PROFILE_FUNC(); + HZ_SCOPE_PERF("DescriptorSetManager::InvalidateAndUpdate"); + + uint32_t currentFrameIndex = Renderer::RT_GetCurrentFrameIndex(); + + // Check for invalidated resources + for (const auto& [set, inputs] : InputResources) + { + for (const auto& [binding, input] : inputs) + { + switch (input.Type) + { + case RenderResourceType::UniformBuffer: + { + //for (uint32_t frameIndex = 0; frameIndex < (uint32_t)WriteDescriptorMap.size(); frameIndex++) + { + const VkDescriptorBufferInfo& bufferInfo = input.Input[0].As()->GetDescriptorBufferInfo(); + if (bufferInfo.buffer != WriteDescriptorMap[currentFrameIndex].at(set).at(binding).ResourceHandles[0]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + case RenderResourceType::UniformBufferSet: + { + //for (uint32_t frameIndex = 0; frameIndex < (uint32_t)WriteDescriptorMap.size(); frameIndex++) + { + const VkDescriptorBufferInfo& bufferInfo = input.Input[0].As()->Get(currentFrameIndex).As()->GetDescriptorBufferInfo(); + if (bufferInfo.buffer != WriteDescriptorMap[currentFrameIndex].at(set).at(binding).ResourceHandles[0]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + case RenderResourceType::StorageBuffer: + { + + //for (uint32_t frameIndex = 0; frameIndex < (uint32_t)WriteDescriptorMap.size(); frameIndex++) + { + const VkDescriptorBufferInfo& bufferInfo = input.Input[0].As()->GetDescriptorBufferInfo(); + if (bufferInfo.buffer != WriteDescriptorMap[currentFrameIndex].at(set).at(binding).ResourceHandles[0]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + case RenderResourceType::StorageBufferSet: + { + //for (uint32_t frameIndex = 0; frameIndex < (uint32_t)WriteDescriptorMap.size(); frameIndex++) + { + const VkDescriptorBufferInfo& bufferInfo = input.Input[0].As()->Get(currentFrameIndex).As()->GetDescriptorBufferInfo(); + if (bufferInfo.buffer != WriteDescriptorMap[currentFrameIndex].at(set).at(binding).ResourceHandles[0]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + case RenderResourceType::Texture2D: + { + for (size_t i = 0; i < input.Input.size(); i++) + { + Ref vulkanTexture = input.Input[i].As(); + if (vulkanTexture == nullptr) + vulkanTexture = Renderer::GetWhiteTexture().As(); // TODO(Yan): error texture + + const VkDescriptorImageInfo& imageInfo = vulkanTexture->GetDescriptorInfoVulkan(); + if (imageInfo.imageView != WriteDescriptorMap[currentFrameIndex].at(set).at(binding).ResourceHandles[i]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + case RenderResourceType::TextureCube: + { + //for (uint32_t frameIndex = 0; frameIndex < (uint32_t)WriteDescriptorMap.size(); frameIndex++) + { + const VkDescriptorImageInfo& imageInfo = input.Input[0].As()->GetDescriptorInfoVulkan(); + if (imageInfo.imageView != WriteDescriptorMap[currentFrameIndex].at(set).at(binding).ResourceHandles[0]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + case RenderResourceType::Image2D: + { + //for (uint32_t frameIndex = 0; frameIndex < (uint32_t)WriteDescriptorMap.size(); frameIndex++) + { + const VkDescriptorImageInfo& imageInfo = *(VkDescriptorImageInfo*)input.Input[0].As()->GetDescriptorInfo(); + if (imageInfo.imageView != WriteDescriptorMap[currentFrameIndex].at(set).at(binding).ResourceHandles[0]) + { + InvalidatedInputResources[set][binding] = input; + break; + } + } + break; + } + } + } + } + + // Nothing to do + if (InvalidatedInputResources.empty()) + return; + + auto bufferSets = HasBufferSets(); + bool perFrameInFlight = !bufferSets.empty(); + perFrameInFlight = true; // always + uint32_t descriptorSetCount = Renderer::GetConfig().FramesInFlight; + if (!perFrameInFlight) + descriptorSetCount = 1; + + + // TODO(Yan): handle these if they fail (although Vulkan will probably give us a validation error if they do anyway) + for (const auto& [set, setData] : InvalidatedInputResources) + { + uint32_t descriptorCountInSet = bufferSets.find(set) != bufferSets.end() ? descriptorSetCount : 1; + //for (uint32_t frameIndex = currentFrameIndex; frameIndex < descriptorSetCount; frameIndex++) + uint32_t frameIndex = perFrameInFlight ? currentFrameIndex : 0; + { + // Go through every resource here and call vkUpdateDescriptorSets with write descriptors + // If we don't have valid buffers/images to bind to here, that's an error and needs to be + // probably handled by putting in some error resources, otherwise we'll crash + std::vector writeDescriptorsToUpdate; + writeDescriptorsToUpdate.reserve(setData.size()); + std::vector> imageInfoStorage; + uint32_t imageInfoStorageIndex = 0; + for (const auto& [binding, input] : setData) + { + // Update stored write descriptor + auto& wd = WriteDescriptorMap[frameIndex].at(set).at(binding); + VkWriteDescriptorSet& writeDescriptor = wd.WriteDescriptorSet; + switch (input.Type) + { + case RenderResourceType::UniformBuffer: + { + Ref buffer = input.Input[0].As(); + writeDescriptor.pBufferInfo = &buffer->GetDescriptorBufferInfo(); + wd.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + break; + } + case RenderResourceType::UniformBufferSet: + { + Ref buffer = input.Input[0].As(); + writeDescriptor.pBufferInfo = &buffer->Get(frameIndex).As()->GetDescriptorBufferInfo(); + wd.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + break; + } + case RenderResourceType::StorageBuffer: + { + Ref buffer = input.Input[0].As(); + writeDescriptor.pBufferInfo = &buffer->GetDescriptorBufferInfo(); + wd.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + break; + } + case RenderResourceType::StorageBufferSet: + { + Ref buffer = input.Input[0].As(); + writeDescriptor.pBufferInfo = &buffer->Get(frameIndex).As()->GetDescriptorBufferInfo(); + wd.ResourceHandles[0] = writeDescriptor.pBufferInfo->buffer; + break; + } + case RenderResourceType::Texture2D: + { + + if (input.Input.size() > 1) + { + imageInfoStorage.emplace_back(input.Input.size()); + for (size_t i = 0; i < input.Input.size(); i++) + { + Ref texture = input.Input[i].As(); + imageInfoStorage[imageInfoStorageIndex][i] = texture->GetDescriptorInfoVulkan(); + wd.ResourceHandles[i] = imageInfoStorage[imageInfoStorageIndex][i].imageView; + } + writeDescriptor.pImageInfo = imageInfoStorage[imageInfoStorageIndex].data(); + imageInfoStorageIndex++; + } + else + { + Ref texture = input.Input[0].As(); + writeDescriptor.pImageInfo = &texture->GetDescriptorInfoVulkan(); + wd.ResourceHandles[0] = writeDescriptor.pImageInfo->imageView; + } + + break; + } + case RenderResourceType::TextureCube: + { + Ref texture = input.Input[0].As(); + writeDescriptor.pImageInfo = &texture->GetDescriptorInfoVulkan(); + wd.ResourceHandles[0] = writeDescriptor.pImageInfo->imageView; + break; + } + case RenderResourceType::Image2D: + { + Ref image = input.Input[0].As(); + writeDescriptor.pImageInfo = (VkDescriptorImageInfo*)image->GetDescriptorInfo(); + HZ_CORE_VERIFY(writeDescriptor.pImageInfo->imageView); + wd.ResourceHandles[0] = writeDescriptor.pImageInfo->imageView; + break; + } + } + writeDescriptorsToUpdate.emplace_back(writeDescriptor); + } + // HZ_CORE_INFO_TAG("Renderer", "RenderPass::Prepare ({}) - updating {} descriptors in set {} (frameIndex={})", m_Specification.DebugName, writeDescriptorsToUpdate.size(), set, frameIndex); + HZ_CORE_INFO_TAG("Renderer", "DescriptorSetManager::InvalidateAndUpdate ({}) - updating {} descriptors in set {} (frameIndex={})", m_Specification.DebugName, writeDescriptorsToUpdate.size(), set, frameIndex); + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + vkUpdateDescriptorSets(device, (uint32_t)writeDescriptorsToUpdate.size(), writeDescriptorsToUpdate.data(), 0, nullptr); + } + } + + InvalidatedInputResources.clear(); +#endif + } + + bool DescriptorSetManager::HasDescriptorSets() const + { + return !m_BindingSets.empty() && !m_BindingSets[0].empty(); + } + + uint32_t DescriptorSetManager::GetFirstSetIndex() const + { + if (InputResources.empty()) + return UINT32_MAX; + + // Return first key (key == descriptor set index) + return InputResources.begin()->first; + } + + nvrhi::BindingSetHandle DescriptorSetManager::GetBindingSet(uint32_t frameIndex) const + { + SE_CORE_ASSERT(!m_BindingSets.empty()); + + if (frameIndex > 0 && m_BindingSets.size() == 1) + frameIndex = 0; // Frame index is irrelevant for this type of render pass + + if (m_BindingSets[frameIndex].empty()) + return nullptr; + + return m_BindingSets[frameIndex][0]; + } + + nvrhi::BindingSetVector DescriptorSetManager::GetBindingSets(uint32_t frameIndex) const + { + SE_CORE_ASSERT(!m_BindingSets.empty()); + + if (frameIndex > 0 && m_BindingSets.size() == 1) + frameIndex = 0; // Frame index is irrelevant for this type of render pass + + nvrhi::BindingSetVector result(m_BindingSets[frameIndex].size()); + for (size_t i = 0; i < result.size(); i++) + result[i] = m_BindingSets[frameIndex][i]; + + return result; + } + + bool DescriptorSetManager::IsInputValid(std::string_view name) const + { + std::string nameStr(name); + return InputDeclarations.find(nameStr) != InputDeclarations.end(); + } + + const RenderInputDeclaration* DescriptorSetManager::GetInputDeclaration(std::string_view name) const + { + std::string nameStr(name); + if (InputDeclarations.find(nameStr) == InputDeclarations.end()) + return nullptr; + + const RenderInputDeclaration& decl = InputDeclarations.at(nameStr); + return &decl; + } + + + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/DescriptorSetManager.h b/StarEngine/src/StarEngine/Platform/Vulkan/DescriptorSetManager.h new file mode 100644 index 00000000..4434ef77 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/DescriptorSetManager.h @@ -0,0 +1,234 @@ +#pragma once + +#include "StarEngine/Renderer/UniformBufferSet.h" +#include "StarEngine/Renderer/StorageBufferSet.h" +#include "StarEngine/Renderer/Image.h" +#include "StarEngine/Renderer/Texture.h" + +#include "StarEngine/Platform/Vulkan/VulkanShader.h" + +#include + +namespace StarEngine { + + // TODO(Yan): + // - Maybe rename from RenderPassXXX to DescriptorXXX or something more + // generic, because these are also used for compute & materials + + struct RenderPassInput + { + static constexpr uint32_t MAX_ARRAY_ELEMENTS = 32; + using InputArray = nvrhi::static_vector, MAX_ARRAY_ELEMENTS>; + + RenderResourceType Type = RenderResourceType::None; + InputArray Input; + + RenderPassInput() = default; + + RenderPassInput(Ref uniformBuffer) + : Type(RenderResourceType::UniformBuffer), Input(InputArray{ uniformBuffer }) + {} + + RenderPassInput(Ref uniformBufferSet) + : Type(RenderResourceType::UniformBufferSet), Input(InputArray{ uniformBufferSet }) + {} + + RenderPassInput(Ref storageBuffer) + : Type(RenderResourceType::StorageBuffer), Input(InputArray{ storageBuffer }) + {} + + RenderPassInput(Ref storageBufferSet) + : Type(RenderResourceType::StorageBufferSet), Input(InputArray{ storageBufferSet }) + {} + + RenderPassInput(Ref texture) + : Type(RenderResourceType::Texture2D), Input(InputArray{ texture }) + {} + + RenderPassInput(Ref texture) + : Type(RenderResourceType::TextureCube), Input(InputArray{ texture }) + {} + + RenderPassInput(Ref image) + : Type(RenderResourceType::Image2D), Input(InputArray{ image }) + {} + + void Set(Ref uniformBuffer, uint32_t index = 0) + { + Type = RenderResourceType::UniformBuffer; + Input[index] = uniformBuffer; + } + + void Set(Ref uniformBufferSet, uint32_t index = 0) + { + Type = RenderResourceType::UniformBufferSet; + Input[index] = uniformBufferSet; + } + + void Set(Ref storageBuffer, uint32_t index = 0) + { + Type = RenderResourceType::StorageBuffer; + Input[index] = storageBuffer; + } + + void Set(Ref storageBufferSet, uint32_t index = 0) + { + Type = RenderResourceType::StorageBufferSet; + Input[index] = storageBufferSet; + } + + void Set(Ref texture, uint32_t index = 0) + { + Type = RenderResourceType::Texture2D; + Input[index] = texture; + } + + void Set(Ref texture, uint32_t index = 0) + { + Type = RenderResourceType::TextureCube; + Input[index] = texture; + } + + void Set(Ref image, uint32_t index = 0) + { + Type = RenderResourceType::Image2D; + Input[index] = image; + } + + void Set(Ref image, uint32_t index = 0) + { + Type = RenderResourceType::Image2D; + Input[index] = image; + } + + void Set(Ref sampler, uint32_t index = 0) + { + Type = RenderResourceType::Sampler; + Input[index] = sampler; + } + + }; + + inline bool IsCompatibleInput(RenderResourceType inputResource, RenderInputType inputType) + { + switch (inputType) + { + case RenderInputType::ImageSampler: + { + return inputResource == RenderResourceType::Sampler; + } + case RenderInputType::ImageSampler2D: + { + return inputResource == RenderResourceType::Texture2D || inputResource == RenderResourceType::Image2D; + } + case RenderInputType::ImageSampler3D: + { + return inputResource == RenderResourceType::TextureCube; + } + case RenderInputType::StorageImage2D: + { + return inputResource == RenderResourceType::Image2D; + } + case RenderInputType::UniformBuffer: + { + return inputResource == RenderResourceType::UniformBuffer || inputResource == RenderResourceType::UniformBufferSet; + } + case RenderInputType::StorageBuffer: + { + return inputResource == RenderResourceType::StorageBuffer || inputResource == RenderResourceType::StorageBufferSet; + } + } + return false; + } + + struct DescriptorSetManagerSpecification + { + Ref Shader; + std::string DebugName; + + // Which descriptor sets should be managed + uint32_t StartSet = 0, EndSet = 3; + + bool IsDynamic = true; // Automatically check resources for change + bool DefaultResources = false; + }; + + struct DescriptorSetManager + { + enum class State + { + None = 0, + Pending = 1, + Ready = 2 + }; + + // + // Input Resources (map of set->binding->resource) + // + // Invalidated input resources will attempt to be assigned on Renderer::BeginRenderPass + // This is useful for resources that may not exist at RenderPass creation but will be + // present during actual rendering + std::map> InputResources; + std::map> InvalidatedInputResources; + std::map InputDeclarations; + + DescriptorSetManager() = default; + DescriptorSetManager(const DescriptorSetManager& other); + DescriptorSetManager(const DescriptorSetManagerSpecification& specification); + static DescriptorSetManager Copy(const DescriptorSetManager& other); + + void SetInput(std::string_view name, Ref uniformBufferSet); + void SetInput(std::string_view name, Ref uniformBuffer); + void SetInput(std::string_view name, Ref storageBufferSet); + void SetInput(std::string_view name, Ref storageBuffer); + void SetInput(std::string_view name, Ref texture, uint32_t index = 0); + void SetInput(std::string_view name, Ref textureCube); + void SetInput(std::string_view name, Ref image); + void SetInput(std::string_view name, Ref image); + void SetInput(std::string_view name, Ref sampler); + + template + Ref GetInput(std::string_view name) + { + const RenderInputDeclaration* decl = GetInputDeclaration(name); + if (decl) + { + auto setIt = InputResources.find(decl->Set); + if (setIt != InputResources.end()) + { + auto resourceIt = setIt->second.find(decl->Binding); + if (resourceIt != setIt->second.end()) + return resourceIt->second.Input[0].As(); + } + } + return nullptr; + } + + bool IsInvalidated(uint32_t set, uint32_t binding) const; + bool Validate(); + void Bake(); + + std::set HasBufferSets() const; + void InvalidateAndUpdate(); + + bool HasDescriptorSets() const; + uint32_t GetFirstSetIndex() const; + nvrhi::BindingSetHandle GetBindingSet(uint32_t frameIndex) const; + nvrhi::BindingSetVector GetBindingSets(uint32_t frameIndex) const; + bool IsInputValid(std::string_view name) const; + const RenderInputDeclaration* GetInputDeclaration(std::string_view name) const; + private: + void Init(); + private: + DescriptorSetManagerSpecification m_Specification; + State m_State = State::None; + + // Per-frame in flight + nvrhi::static_vector, 3> m_BindingSets; + // Frame->set->binding + static constexpr uint32_t MAX_ARRAY_ELEMENTS = 32; + nvrhi::static_vector>>, 3> m_BindingSetHandles; + + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/GlslIncluder.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/GlslIncluder.cpp new file mode 100644 index 00000000..842ff241 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/GlslIncluder.cpp @@ -0,0 +1,67 @@ +#include "sepch.h" +#include "GlslIncluder.h" + +#include "StarEngine/Utilities/StringUtils.h" +#include +#include "StarEngine/Core/Hash.h" +#include "StarEngine/Platform/Vulkan/VulkanShader.h" + + +namespace StarEngine { + + GlslIncluder::GlslIncluder(const shaderc_util::FileFinder* file_finder) + : m_FileFinder(*file_finder) + { + } + + GlslIncluder::~GlslIncluder() = default; + + shaderc_include_result* GlslIncluder::GetInclude(const char* requestedPath, const shaderc_include_type type, const char* requestingPath, const size_t includeDepth) + { + const std::filesystem::path requestedFullPath = (type == shaderc_include_type_relative) + ? m_FileFinder.FindRelativeReadableFilepath(requestingPath, requestedPath) + : m_FileFinder.FindReadableFilepath(requestedPath); + + auto& [source, sourceHash, stages, isGuarded] = m_HeaderCache[requestedFullPath.string()]; + + if (source.empty()) + { + source = Utils::ReadFileAndSkipBOM(requestedFullPath); + if (source.empty()) + SE_CORE_ERROR("Failed to load included file: {} in {}.", requestedFullPath, requestingPath); + sourceHash = Hash::GenerateFNVHash(source.c_str()); + + // Can clear "source" in case it has already been included in this stage and is guarded. + stages = ShaderPreprocessor::PreprocessHeader(source, isGuarded, m_ParsedSpecialMacros, m_includeData, requestedFullPath); + } + else if (isGuarded) + { + source.clear(); + } + + // Does not emplace if it finds the same include path and same header hash value. + m_includeData.emplace(IncludeData{ requestedFullPath, includeDepth, type == shaderc_include_type_relative, isGuarded, sourceHash, stages }); + + auto* const container = new std::array; + (*container)[0] = requestedPath; + (*container)[1] = source; + auto* const data = new shaderc_include_result; + + data->user_data = container; + + data->source_name = (*container)[0].data(); + data->source_name_length = (*container)[0].size(); + + data->content = (*container)[1].data(); + data->content_length = (*container)[1].size(); + + return data; + } + + void GlslIncluder::ReleaseInclude(shaderc_include_result* data) + { + delete static_cast*>(data->user_data); + delete data; + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/GlslIncluder.h b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/GlslIncluder.h new file mode 100644 index 00000000..f3a84003 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/GlslIncluder.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include "ShaderPreprocessor.h" + +namespace StarEngine { + struct IncludeData; + + class GlslIncluder : public shaderc::CompileOptions::IncluderInterface + { + public: + explicit GlslIncluder(const shaderc_util::FileFinder* file_finder); + + ~GlslIncluder() override; + + shaderc_include_result* GetInclude(const char* requestedPath, shaderc_include_type type, const char* requestingPath, size_t includeDepth) override; + + void ReleaseInclude(shaderc_include_result* data) override; + + std::unordered_set&& GetIncludeData() { return std::move(m_includeData); } + std::unordered_set&& GetParsedSpecialMacros() { return std::move(m_ParsedSpecialMacros); } + + private: + // Used by GetInclude() to get the full filepath. + const shaderc_util::FileFinder& m_FileFinder; + std::unordered_set m_includeData; + std::unordered_set m_ParsedSpecialMacros; + std::unordered_map m_HeaderCache; + }; +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/HlslIncluder.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/HlslIncluder.cpp new file mode 100644 index 00000000..39ccae0f --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/HlslIncluder.cpp @@ -0,0 +1,55 @@ +#include "sepch.h" +#include "HlslIncluder.h" + +#include "ShaderPreprocessor.h" + +#include "StarEngine/Core/Hash.h" +#include "StarEngine/Utilities/StringUtils.h" + +namespace StarEngine +{ + HRESULT HlslIncluder::LoadSource(LPCWSTR pFilename, IDxcBlob** ppIncludeSource) + { + static IDxcUtils* pUtils = nullptr; + if (!pUtils) + { + DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&pUtils)); + HRESULT result = pUtils->CreateDefaultIncludeHandler(&s_DefaultIncludeHandler); + SE_CORE_ASSERT(!FAILED(result), "Failed to create default include handler!"); + } + + const std::filesystem::path filePath = pFilename; + auto& [source, sourceHash, stages, isGuarded] = m_HeaderCache[filePath.string()]; + + if(source.empty()) + { + source = Utils::ReadFileAndSkipBOM(filePath); + if (source.empty()) + { + // Note(Karim): No error logging because dxc tries multiple include + // directories with the same file until it finds it. + *ppIncludeSource = nullptr; + return S_FALSE; + } + + sourceHash = Hash::GenerateFNVHash(source.c_str()); + + // Can clear "source" in case it has already been included in this stage. + stages = ShaderPreprocessor::PreprocessHeader(source, isGuarded, m_ParsedSpecialMacros, m_includeData, filePath); + } + else if (isGuarded) + { + source.clear(); + } + + //TODO(Karim): Get real values for IncludeDepth and IsRelative? + m_includeData.emplace(IncludeData{ filePath, 0, false, isGuarded, sourceHash, stages }); + + IDxcBlobEncoding* pEncoding; + pUtils->CreateBlob(source.data(), (uint32_t)source.size(), CP_UTF8, &pEncoding); + + *ppIncludeSource = pEncoding; + return S_OK; + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/HlslIncluder.h b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/HlslIncluder.h new file mode 100644 index 00000000..629441bd --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/HlslIncluder.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "ShaderPreprocessor.h" + +namespace StarEngine +{ + struct IncludeData; + + class HlslIncluder : public IDxcIncludeHandler + { + public: + HRESULT LoadSource(LPCWSTR pFilename, IDxcBlob** ppIncludeSource) override; + HRESULT QueryInterface(const IID& riid, void** ppvObject) override + { + return s_DefaultIncludeHandler->QueryInterface(riid, ppvObject); + } + std::unordered_set&& GetIncludeData() { return std::move(m_includeData); } + std::unordered_set&& GetParsedSpecialMacros() { return std::move(m_ParsedSpecialMacros); } + + ULONG AddRef() override { return 0; } + + ULONG Release() override { return 0; } + + private: + + inline static IDxcIncludeHandler* s_DefaultIncludeHandler = nullptr; + std::unordered_set m_includeData; + std::unordered_set m_ParsedSpecialMacros; + std::unordered_map m_HeaderCache; + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/ShaderPreprocessor.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/ShaderPreprocessor.cpp new file mode 100644 index 00000000..4c8bc09a --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/ShaderPreprocessor.cpp @@ -0,0 +1,6 @@ +#include "sepch.h" +#include "ShaderPreprocessor.h" + +namespace StarEngine { + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/ShaderPreprocessor.h b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/ShaderPreprocessor.h new file mode 100644 index 00000000..d631ae93 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/ShaderPreprocessing/ShaderPreprocessor.h @@ -0,0 +1,356 @@ +#pragma once + +#include "StarEngine/Platform/Vulkan/VulkanShaderUtils.h" +#include "StarEngine/Renderer/Shader.h" +#include "StarEngine/Utilities/StringUtils.h" + +#include +#include +#include +#include + +enum VkShaderStageFlagBits; + +namespace StarEngine { + namespace PreprocessUtils { + template + bool ContainsHeaderGuard(std::string& header) + { + size_t pos = header.find('#'); + while (pos != std::string::npos) + { + const size_t endOfLine = header.find_first_of("\r\n", pos) + 1; + auto tokens = Utils::SplitStringAndKeepDelims(header.substr(pos, endOfLine - pos)); + auto it = tokens.begin(); + + if (*(++it) == "pragma") + { + if (*(++it) == "once") + { + if constexpr (RemoveHeaderGuard) + header.erase(pos, endOfLine - pos); + return true; + } + } + pos = header.find('#', pos + 1); + } + return false; + } + + // From https://wandbox.org/permlink/iXC7DWaU8Tk8jrf3 and is modified. + enum class State : char { SlashOC, StarIC, SingleLineComment, MultiLineComment, NotAComment }; + + template + void CopyWithoutComments(InputIt first, InputIt last, OutputIt out) + { + State state = State::NotAComment; + + while (first != last) + { + switch (state) + { + case State::SlashOC: + if (*first == '/') state = State::SingleLineComment; + else if (*first == '*') state = State::MultiLineComment; + else + { + state = State::NotAComment; + *out++ = '/'; + *out++ = *first; + } + break; + case State::StarIC: + if (*first == '/') state = State::NotAComment; + else state = State::MultiLineComment; + break; + case State::NotAComment: + if (*first == '/') state = State::SlashOC; + else *out++ = *first; + break; + case State::SingleLineComment: + if (*first == '\n') + { + state = State::NotAComment; + *out++ = '\n'; + } + break; + case State::MultiLineComment: + if (*first == '*') state = State::StarIC; + else if (*first == '\n') *out++ = '\n'; + break; + } + ++first; + } + } + } + + struct IncludeData + { + std::filesystem::path IncludedFilePath {}; + size_t IncludeDepth {}; + bool IsRelative { false }; + bool IsGuarded { false }; + uint32_t HashValue {}; + + nvrhi::ShaderType IncludedStage{}; + + inline bool operator==(const IncludeData& other) const noexcept + { + return this->IncludedFilePath == other.IncludedFilePath && this->HashValue == other.HashValue; + } + }; + + struct HeaderCache + { + std::string Source; + uint32_t SourceHash; + nvrhi::ShaderType Stages; + bool IsGuarded; + }; +} + +namespace std { + template<> + struct hash + { + size_t operator()(const StarEngine::IncludeData& data) const noexcept + { + return std::filesystem::hash_value(data.IncludedFilePath) ^ data.HashValue; + } + }; +} + +namespace StarEngine { + class ShaderPreprocessor + { + public: + template + static nvrhi::ShaderType PreprocessHeader(std::string& contents, bool& isGuarded, std::unordered_set& specialMacros, const std::unordered_set& includeData, const std::filesystem::path& fullPath); + template + static std::map PreprocessShader(const std::string& source, std::unordered_set& specialMacros); + }; + + template + nvrhi::ShaderType ShaderPreprocessor::PreprocessHeader(std::string& contents, bool& isGuarded, std::unordered_set& specialMacros, const std::unordered_set& includeData, const std::filesystem::path& fullPath) + { + std::stringstream sourceStream; + PreprocessUtils::CopyWithoutComments(contents.begin(), contents.end(), std::ostream_iterator(sourceStream)); + contents = sourceStream.str(); + + nvrhi::ShaderType stagesInHeader = nvrhi::ShaderType::None; + + //Removes header guard in GLSL only. + isGuarded = PreprocessUtils::ContainsHeaderGuard(contents); + + uint32_t stageCount = 0; + size_t startOfShaderStage = contents.find('#', 0); + + while (startOfShaderStage != std::string::npos) + { + const size_t endOfLine = contents.find_first_of("\r\n", startOfShaderStage) + 1; + // Parse stage. example: #pragma stage:vert + auto tokens = Utils::SplitStringAndKeepDelims(contents.substr(startOfShaderStage, endOfLine - startOfShaderStage)); + + uint32_t index = 0; + // Pre-processor directives + if (tokens[index] == "#") + { + ++index; + // Pragmas + if (tokens[index] == "pragma") + { + ++index; + // Stages + if (tokens[index] == "stage") + { + SE_CORE_VERIFY(tokens[++index] == ":", "Stage pragma is invalid"); + + // Skipped ':' + const std::string_view stage(tokens[++index]); + SE_CORE_VERIFY(stage == "vert" || stage == "frag" || stage == "comp", "Invalid shader type specified"); + nvrhi::ShaderType foundStage = ShaderUtils::PreprocessorStageToShaderStage(stage); + + const bool alreadyIncluded = std::find_if(includeData.begin(), includeData.end(), [fullPath, foundStage](const IncludeData& data) + { + return data.IncludedFilePath == fullPath.string() && !bool((uint16_t)foundStage & (uint16_t)data.IncludedStage); + }) != includeData.end(); + + if (isGuarded && alreadyIncluded) + contents.clear(); + else if (!isGuarded && alreadyIncluded) + SE_CORE_WARN("\"{}\" Header does not contain a header guard (#pragma once).", fullPath); + + // Add #endif + if (stageCount == 0) + contents.replace(startOfShaderStage, endOfLine - startOfShaderStage, std::format("#ifdef {}\r\n", ShaderUtils::ShaderStageToShaderMacro(foundStage))); + else // Add stage macro instead of stage pragma, both #endif and #ifdef must be in the same line, hence no '\n' + contents.replace(startOfShaderStage, endOfLine - startOfShaderStage, std::format("#endif\r\n#ifdef {}", ShaderUtils::ShaderStageToShaderMacro(foundStage))); + + *(uint16_t*)&stagesInHeader |= (uint16_t)foundStage; + stageCount++; + } + } + else if (tokens[index] == "ifdef") + { + ++index; + if (tokens[index].rfind("__SE_", 0) == 0) // StarEngine special macros start with "__SE_" + { + specialMacros.emplace(tokens[index]); + } + } + else if (tokens[index] == "if" || tokens[index] == "define") // Consider "#if defined()" too? + { + ++index; + for (size_t i = index; i < tokens.size(); ++i) + { + if (tokens[i].rfind("__SE_", 0) == 0) // StarEngine special macros start with "__SE_" + { + specialMacros.emplace(tokens[i]); + } + } + } + } + + startOfShaderStage = contents.find('#', startOfShaderStage + 1); + } + if (stageCount) + contents.append("\n#endif"); + else + { + const bool alreadyIncluded = std::find_if(includeData.begin(), includeData.end(), [fullPath](const IncludeData& data) + { + return data.IncludedFilePath == fullPath; + }) != includeData.end(); + if (isGuarded && alreadyIncluded) + contents.clear(); + else if (!isGuarded && alreadyIncluded) + SE_CORE_WARN("\"{}\" Header does not contain a header guard (#pragma once)", fullPath); + } + + + return stagesInHeader; + } + + template + std::map ShaderPreprocessor::PreprocessShader(const std::string& source, std::unordered_set& specialMacros) + { + std::stringstream sourceStream; + PreprocessUtils::CopyWithoutComments(source.begin(), source.end(), std::ostream_iterator(sourceStream)); + std::string newSource = sourceStream.str(); + + std::map shaderSources; + std::vector> stagePositions; + SE_CORE_ASSERT(newSource.size(), "Shader is empty!"); + + size_t startOfStage = 0; + size_t pos = newSource.find('#'); + + //Check first #version + if constexpr (Lang == ShaderUtils::SourceLang::GLSL) + { + const size_t endOfLine = newSource.find_first_of("\r\n", pos) + 1; + const std::vector tokens = Utils::SplitStringAndKeepDelims(newSource.substr(pos, endOfLine - pos)); + SE_CORE_VERIFY(tokens.size() >= 3 && tokens[1] == "version", "Invalid #version encountered or #version is NOT encounted first."); + pos = newSource.find('#', pos + 1); + } + + while (pos != std::string::npos) + { + + const size_t endOfLine = newSource.find_first_of("\r\n", pos) + 1; + std::vector tokens = Utils::SplitStringAndKeepDelims(newSource.substr(pos, endOfLine - pos)); + + size_t index = 1; // Skip # + + + if (tokens[index] == "pragma") // Parse stage. example: #pragma stage : vert + { + ++index; + if (tokens[index] == "stage") + { + ++index; + // Jump over ':' + SE_CORE_VERIFY(tokens[index] == ":", "Stage pragma is invalid"); + ++index; + + const std::string_view stage = tokens[index]; + SE_CORE_VERIFY(stage == "vert" || stage == "frag" || stage == "comp", "Invalid shader type specified"); + auto shaderStage = ShaderUtils::PreprocessorStageToShaderStage(stage); + + if constexpr (Lang == ShaderUtils::SourceLang::HLSL) + { + startOfStage = pos; + } + stagePositions.emplace_back(shaderStage, startOfStage); + } + } + else if (tokens[index] == "ifdef") + { + ++index; + if (tokens[index].rfind("__SE_", 0) == 0) // StarEngine special macros start with "__SE_" + { + specialMacros.emplace(tokens[index]); + } + } + else if (tokens[index] == "if" || tokens[index] == "define") + { + ++index; + for (size_t i = index; i < tokens.size(); ++i) + { + if (tokens[i].rfind("__SE_", 0) == 0) // StarEngine special macros start with "__SE_" + { + specialMacros.emplace(tokens[i]); + } + } + } + else if constexpr (Lang == ShaderUtils::SourceLang::GLSL) + { + if (tokens[index] == "version") + { + ++index; + startOfStage = pos; + } + } + + pos = newSource.find('#', pos + 1); + } + + SE_CORE_VERIFY(stagePositions.size(), "Could not pre-process shader! There are no known stages defined in file."); + auto& [firstStage, firstStagePos] = stagePositions[0]; + if (stagePositions.size() > 1) + { + //Get first stage + const std::string firstStageStr = newSource.substr(0, stagePositions[1].second); + size_t lineCount = std::count(firstStageStr.begin(), firstStageStr.end(), '\n') + 1; + shaderSources[firstStage] = firstStageStr; + + + //Get stages in the middle + for (size_t i = 1; i < stagePositions.size() - 1; ++i) + { + auto& [stage, stagePos] = stagePositions[i]; + std::string stageStr = newSource.substr(stagePos, stagePositions[i + 1].second - stagePos); + const size_t secondLinePos = stageStr.find_first_of('\n', 1) + 1; + stageStr.insert(secondLinePos, std::format("#line {}\n", lineCount)); + shaderSources[stage] = stageStr; + lineCount += std::count(stageStr.begin(), stageStr.end(), '\n') + 1; + } + + //Get last stage + auto& [stage, stagePos] = stagePositions[stagePositions.size() - 1]; + std::string lastStageStr = newSource.substr(stagePos); + const size_t secondLinePos = lastStageStr.find_first_of('\n', 1) + 1; + lastStageStr.insert(secondLinePos, std::format("#line {}\n", lineCount + 1)); + shaderSources[stage] = lastStageStr; + } + else + { + shaderSources[firstStage] = newSource; + } + + return shaderSources; + } +} + + + diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCache.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCache.cpp new file mode 100644 index 00000000..04f83b1b --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCache.cpp @@ -0,0 +1,159 @@ +#include "sepch.h" +#include "VulkanShaderCache.h" +#include "StarEngine/Core/Hash.h" +#include "StarEngine/Platform/Vulkan/VulkanShaderUtils.h" + +#include "nvrhi/utils.h" + +#include "yaml-cpp/yaml.h" + +#include "ShaderPreprocessing/ShaderPreprocessor.h" +#include "StarEngine/Utilities/SerializationMacros.h" + +namespace StarEngine { + + static const char* s_ShaderRegistryPath = "Resources/Cache/Shader/ShaderRegistry.cache"; + + nvrhi::ShaderType VulkanShaderCache::HasChanged(Ref shader) + { + std::map> shaderCache; + + Deserialize(shaderCache); + + nvrhi::ShaderType changedStages = nvrhi::ShaderType::None; + const bool shaderNotCached = shaderCache.find(shader->m_ShaderSourcePath.string()) == shaderCache.end(); + + for (const auto& [stage, stageSource] : shader->m_ShaderSource) + { + // Keep in mind that we're using the [] operator. + // Which means that we add the stage if it's not already there. + if (shaderNotCached || shader->m_StagesMetadata.at(stage) != shaderCache[shader->m_ShaderSourcePath.string()][stage]) + { + shaderCache[shader->m_ShaderSourcePath.string()][stage] = shader->m_StagesMetadata.at(stage); + *(uint16_t*)&changedStages |= (uint16_t)stage; + } + } + + // Update cache in case we added a stage but didn't remove the deleted(in file) stages + shaderCache.at(shader->m_ShaderSourcePath.string()) = shader->m_StagesMetadata; + + if (changedStages != nvrhi::ShaderType::None) + Serialize(shaderCache); + + return changedStages; + } + + + void VulkanShaderCache::Serialize(const std::map>& shaderCache) + { + YAML::Emitter out; + + out << YAML::BeginMap << YAML::Key << "ShaderRegistry" << YAML::BeginSeq;// ShaderRegistry_ + + for (auto& [filepath, shader] : shaderCache) + { + out << YAML::BeginMap; // Shader_ + + out << YAML::Key << "ShaderPath" << YAML::Value << filepath; + + out << YAML::Key << "Stages" << YAML::BeginSeq; // Stages_ + + for (auto& [stage, stageData] : shader) + { + out << YAML::BeginMap; // Stage_ + + out << YAML::Key << "Stage" << YAML::Value << nvrhi::utils::ShaderStageToString(stage); + out << YAML::Key << "StageHash" << YAML::Value << stageData.HashValue; + + out << YAML::Key << "Headers" << YAML::BeginSeq; // Headers_ + for (auto& header : stageData.Headers) + { + + out << YAML::BeginMap; + + SE_SERIALIZE_PROPERTY(HeaderPath, header.IncludedFilePath.string(), out); + SE_SERIALIZE_PROPERTY(IncludeDepth, header.IncludeDepth, out); + SE_SERIALIZE_PROPERTY(IsRelative, header.IsRelative, out); + SE_SERIALIZE_PROPERTY(IsGaurded, header.IsGuarded, out); + SE_SERIALIZE_PROPERTY(HashValue, header.HashValue, out); + + out << YAML::EndMap; + } + out << YAML::EndSeq; // Headers_ + + out << YAML::EndMap; // Stage_ + } + out << YAML::EndSeq; // Stages_ + out << YAML::EndMap; // Shader_ + + } + out << YAML::EndSeq; // ShaderRegistry_ + out << YAML::EndMap; // File_ + + std::ofstream fout(s_ShaderRegistryPath); + fout << out.c_str(); + } + + void VulkanShaderCache::Deserialize(std::map>& shaderCache) + { + // Read registry + std::ifstream stream(s_ShaderRegistryPath); + if (!stream.good()) + return; + + std::stringstream strStream; + strStream << stream.rdbuf(); + + YAML::Node data = YAML::Load(strStream.str()); + auto handles = data["ShaderRegistry"]; + if (handles.IsNull()) + { + SE_CORE_ERROR("[ShaderCache] Shader Registry is invalid."); + return; + } + + // Old format + if (handles.IsMap()) + { + SE_CORE_ERROR("[ShaderCache] Old Shader Registry format."); + return; + } + + for (auto shader : handles) + { + std::string path; + SE_DESERIALIZE_PROPERTY(ShaderPath, path, shader, std::string()); + for (auto stage : shader["Stages"]) //Stages + { + std::string stageType; + uint32_t stageHash; + SE_DESERIALIZE_PROPERTY(Stage, stageType, stage, std::string()); + SE_DESERIALIZE_PROPERTY(StageHash, stageHash, stage, 0u); + + auto& stageCache = shaderCache[path][nvrhi::utils::ShaderStageFromString(stageType.c_str())]; + stageCache.HashValue = stageHash; + + for (auto header : stage["Headers"]) + { + std::string headerPath; + uint32_t includeDepth; + bool isRelative; + bool isGuarded; + uint32_t hashValue; + SE_DESERIALIZE_PROPERTY(HeaderPath, headerPath, header, std::string()); + SE_DESERIALIZE_PROPERTY(IncludeDepth, includeDepth, header, 0u); + SE_DESERIALIZE_PROPERTY(IsRelative, isRelative, header, false); + SE_DESERIALIZE_PROPERTY(IsGaurded, isGuarded, header, false); + SE_DESERIALIZE_PROPERTY(HashValue, hashValue, header, 0u); + + stageCache.Headers.emplace(IncludeData{ headerPath, includeDepth, isRelative, isGuarded, hashValue }); + + } + + } + + } + + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCache.h b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCache.h new file mode 100644 index 00000000..bb88b87c --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCache.h @@ -0,0 +1,21 @@ +#pragma once + +#include "nvrhi/nvrhi.h" + +#include "VulkanShaderCompiler.h" + +#include +#include + +namespace StarEngine { + + class VulkanShaderCache + { + public: + static nvrhi::ShaderType HasChanged(Ref shader); + private: + static void Serialize(const std::map>& shaderCache); + static void Deserialize(std::map>& shaderCache); + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp new file mode 100644 index 00000000..e8d639cc --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.cpp @@ -0,0 +1,974 @@ +#include "sepch.h" +#include "VulkanShaderCompiler.h" + +#include "VulkanShaderCache.h" + +#include "ShaderPreprocessing/GlslIncluder.h" +#include "ShaderPreprocessing/HlslIncluder.h" + +#include "StarEngine/Core/Hash.h" +#include "StarEngine/Platform/Vulkan/VulkanContext.h" +#include "StarEngine/Platform/Vulkan/VulkanShader.h" +#include "StarEngine/Serialization/FileStream.h" +#include "StarEngine/Utilities/StringUtils.h" +#include "StarEngine/Utilities/FileSystem.h" + +#include "nvrhi/utils.h" + +#include +#include +#include +#include +#include + +#include +#include + +#if defined(SE_PLATFORM_LINUX) +#include +#include +#include +#include +#endif + + +namespace StarEngine { + + static std::unordered_map> s_UniformBuffers; // set -> binding point -> buffer + static std::unordered_map> s_StorageBuffers; // set -> binding point -> buffer + + namespace Utils { + + static const char* GetCacheDirectory() + { + // TODO: make sure the assets directory is valid + return "Resources/Cache/Shader/Vulkan"; + } + + static void CreateCacheDirectoryIfNeeded() + { + std::string cacheDirectory = GetCacheDirectory(); + if (!std::filesystem::exists(cacheDirectory)) + std::filesystem::create_directories(cacheDirectory); + } + + static ShaderUniformType SPIRTypeToShaderUniformType(spirv_cross::SPIRType type) + { + switch (type.basetype) + { + case spirv_cross::SPIRType::Boolean: return ShaderUniformType::Bool; + case spirv_cross::SPIRType::Int: + if (type.vecsize == 1) return ShaderUniformType::Int; + if (type.vecsize == 2) return ShaderUniformType::IVec2; + if (type.vecsize == 3) return ShaderUniformType::IVec3; + if (type.vecsize == 4) return ShaderUniformType::IVec4; + + case spirv_cross::SPIRType::UInt: return ShaderUniformType::UInt; + case spirv_cross::SPIRType::Float: + if (type.columns == 3) return ShaderUniformType::Mat3; + if (type.columns == 4) return ShaderUniformType::Mat4; + + if (type.vecsize == 1) return ShaderUniformType::Float; + if (type.vecsize == 2) return ShaderUniformType::Vec2; + if (type.vecsize == 3) return ShaderUniformType::Vec3; + if (type.vecsize == 4) return ShaderUniformType::Vec4; + break; + } + SE_CORE_ASSERT(false, "Unknown type!"); + return ShaderUniformType::None; + } + } + + VulkanShaderCompiler::VulkanShaderCompiler(const std::filesystem::path& shaderSourcePath, bool disableOptimization) + : m_ShaderSourcePath(shaderSourcePath), m_DisableOptimization(disableOptimization) + { + m_Language = ShaderUtils::ShaderLangFromExtension(shaderSourcePath.extension().string()); + } + + bool VulkanShaderCompiler::Reload(bool forceCompile) + { + m_ShaderSource.clear(); + m_StagesMetadata.clear(); + m_SPIRVDebugData.clear(); + m_SPIRVData.clear(); + + Utils::CreateCacheDirectoryIfNeeded(); + const std::string source = Utils::ReadFileAndSkipBOM(m_ShaderSourcePath); + SE_CORE_VERIFY(source.size(), "Failed to load shader!"); + + SE_CORE_TRACE_TAG("Renderer", "Compiling shader: {}", m_ShaderSourcePath.string()); + m_ShaderSource = PreProcess(source); + const nvrhi::ShaderType changedStages = VulkanShaderCache::HasChanged(this); + + bool compileSucceeded = CompileOrGetVulkanBinaries(m_SPIRVDebugData, m_SPIRVData, changedStages, forceCompile); + if (!compileSucceeded) + { + SE_CORE_ASSERT(false); + SE_CORE_VERIFY(false); + return false; + } + + + // Reflection + if (forceCompile || changedStages != nvrhi::ShaderType::None || !TryReadCachedReflectionData()) + { + ReflectAllShaderStages(m_SPIRVDebugData); + SerializeReflectionData(); + } + + return true; + } + + void VulkanShaderCompiler::ClearUniformBuffers() + { + s_UniformBuffers.clear(); + s_StorageBuffers.clear(); + } + + std::map VulkanShaderCompiler::PreProcess(const std::string& source) + { + switch (m_Language) + { + case ShaderUtils::SourceLang::GLSL: return PreProcessGLSL(source); + case ShaderUtils::SourceLang::HLSL: return PreProcessHLSL(source); + } + + SE_CORE_VERIFY(false); + return {}; + } + + std::map VulkanShaderCompiler::PreProcessGLSL(const std::string& source) + { + std::map shaderSources = ShaderPreprocessor::PreprocessShader(source, m_AcknowledgedMacros); + + static shaderc::Compiler compiler; + + shaderc_util::FileFinder fileFinder; + fileFinder.search_path().emplace_back("Resources/Shaders/Include/GLSL/"); //Main include directory + fileFinder.search_path().emplace_back("Resources/Shaders/Include/Common/"); //Shared include directory + for (auto& [stage, shaderSource] : shaderSources) + { + shaderc::CompileOptions options; + options.AddMacroDefinition("__GLSL__"); + options.AddMacroDefinition(std::string(ShaderUtils::ShaderStageToShaderMacro(stage))); + + const auto& globalMacros = Renderer::GetGlobalShaderMacros(); + for (const auto& [name, value] : globalMacros) + options.AddMacroDefinition(name, value); + + // Deleted by shaderc and created per stage + GlslIncluder* includer = new GlslIncluder(&fileFinder); + + options.SetIncluder(std::unique_ptr(includer)); + const auto preProcessingResult = compiler.PreprocessGlsl(shaderSource, ShaderUtils::ShaderStageToShaderC(stage), m_ShaderSourcePath.string().c_str(), options); + if (preProcessingResult.GetCompilationStatus() != shaderc_compilation_status_success) + SE_CORE_ERROR_TAG("Renderer", std::format("Failed to pre-process \"{}\"'s {} shader.\nError: {}", m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage), preProcessingResult.GetErrorMessage())); + + m_StagesMetadata[stage].HashValue = Hash::GenerateFNVHash(shaderSource); + m_StagesMetadata[stage].Headers = std::move(includer->GetIncludeData()); + + m_AcknowledgedMacros.merge(includer->GetParsedSpecialMacros()); + + shaderSource = std::string(preProcessingResult.begin(), preProcessingResult.end()); + } + return shaderSources; + } + + std::map VulkanShaderCompiler::PreProcessHLSL(const std::string& source) + { + std::map shaderSources = ShaderPreprocessor::PreprocessShader(source, m_AcknowledgedMacros); + +#ifdef SE_PLATFORM_WINDOWS + std::wstring buffer = m_ShaderSourcePath.wstring(); +#else + std::wstring buffer; + buffer.resize(m_ShaderSourcePath.string().size() * 2); + mbstowcs(buffer.data(), m_ShaderSourcePath.string().c_str(), m_ShaderSourcePath.string().size()); +#endif + + std::vector arguments{ buffer.c_str(), L"-P", DXC_ARG_WARNINGS_ARE_ERRORS, + L"-I Resources/Shaders/Include/Common/", + L"-I Resources/Shaders/Include/HLSL/", //Main include directory + L"-D", L"__HLSL__", + }; + + const auto& globalMacros = Renderer::GetGlobalShaderMacros(); + for (const auto& [name, value] : globalMacros) + { + arguments.emplace_back(L"-D"); + arguments.push_back(nullptr); + std::string def; + if (value.size()) + def = std::format("{}={}", name, value); + else + def = name; + + wchar_t* def_buffer = new wchar_t[def.size() + 1]; + mbstowcs(def_buffer, def.c_str(), def.size()); + def_buffer[def.size()] = 0; + arguments[arguments.size() - 1] = def_buffer; + } + + if (!DxcInstances::Compiler) + { +#ifdef SE_PLATFORM_WINDOWS + DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&DxcInstances::Compiler)); + DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&DxcInstances::Utils)); +#endif + } + + for (auto& [stage, shaderSource] : shaderSources) + { +#ifdef SE_PLATFORM_WINDOWS + IDxcBlobEncoding* pSource; + DxcInstances::Utils->CreateBlob(shaderSource.c_str(), (uint32_t)shaderSource.size(), CP_UTF8, &pSource); + + DxcBuffer sourceBuffer; + sourceBuffer.Ptr = pSource->GetBufferPointer(); + sourceBuffer.Size = pSource->GetBufferSize(); + sourceBuffer.Encoding = 0; + + const std::unique_ptr includer = std::make_unique(); + IDxcResult* pCompileResult; + HRESULT err = DxcInstances::Compiler->Compile(&sourceBuffer, arguments.data(), (uint32_t)arguments.size(), includer.get(), IID_PPV_ARGS(&pCompileResult)); + + // Error Handling + std::string error; + const bool failed = FAILED(err); + if (failed) + error = std::format("Failed to pre-process, Error: {}\n", err); + IDxcBlobEncoding* pErrors = nullptr; + pCompileResult->GetErrorBuffer(&pErrors); + if (pErrors->GetBufferPointer() && pErrors->GetBufferSize()) + error.append(std::format("{}\nWhile pre-processing shader file: {} \nAt stage: {}", (char*)pErrors->GetBufferPointer(), m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage))); + + if (error.empty()) + { + // Successful compilation + IDxcBlob* pResult; + pCompileResult->GetResult(&pResult); + + const size_t size = pResult->GetBufferSize(); + shaderSource = (const char*)pResult->GetBufferPointer(); + pResult->Release(); + } + else + { + SE_CORE_ERROR_TAG("Renderer", error); + } + + m_StagesMetadata[stage].HashValue = Hash::GenerateFNVHash(shaderSource); + m_StagesMetadata[stage].Headers = std::move(includer->GetIncludeData()); + + m_AcknowledgedMacros.merge(includer->GetParsedSpecialMacros()); +#else + m_StagesMetadata[stage] = StageData{}; +#endif + } + return shaderSources; + } + + std::string VulkanShaderCompiler::Compile(std::vector& outputBinary, const nvrhi::ShaderType stage, CompilationOptions options) const + { + const std::string& stageSource = m_ShaderSource.at(stage); + + if (m_Language == ShaderUtils::SourceLang::GLSL) + { + static shaderc::Compiler compiler; + shaderc::CompileOptions shaderCOptions; + shaderCOptions.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + shaderCOptions.SetWarningsAsErrors(); + if (options.GenerateDebugInfo) + shaderCOptions.SetGenerateDebugInfo(); + + if (options.Optimize) + shaderCOptions.SetOptimizationLevel(shaderc_optimization_level_performance); + + // Compile shader + const shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(stageSource, ShaderUtils::ShaderStageToShaderC(stage), m_ShaderSourcePath.string().c_str(), shaderCOptions); + + if (module.GetCompilationStatus() != shaderc_compilation_status_success) + return std::format("{}While compiling shader file: {} \nAt stage: {}", module.GetErrorMessage(), m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage)); + + outputBinary = std::vector(module.begin(), module.end()); + return {}; // Success + } + else if (m_Language == ShaderUtils::SourceLang::HLSL) + { +#ifdef SE_PLATFORM_WINDOWS + std::wstring buffer = m_ShaderSourcePath.wstring(); + std::vector arguments{ buffer.c_str(), L"-E", L"main", L"-T",ShaderUtils::HLSLShaderProfile(stage), L"-spirv", L"-fspv-target-env=vulkan1.2", + DXC_ARG_PACK_MATRIX_COLUMN_MAJOR, DXC_ARG_WARNINGS_ARE_ERRORS + + // TODO(Karim): L"-fspv-reflect" causes a validation error about SPV_GOOGLE_hlsl_functionality1 + // Without this argument, not much info will be in Nsight. + //L"-fspv-reflect", + }; + + if (options.GenerateDebugInfo) + { + arguments.emplace_back(L"-Qembed_debug"); + arguments.emplace_back(DXC_ARG_DEBUG); + } + + if ((uint16_t)stage & ((uint16_t)nvrhi::ShaderType::Vertex | (uint16_t)nvrhi::ShaderType::Hull | (uint16_t)nvrhi::ShaderType::Geometry)) + arguments.push_back(L"-fvk-invert-y"); + + IDxcBlobEncoding* pSource; + DxcInstances::Utils->CreateBlob(stageSource.c_str(), (uint32_t)stageSource.size(), CP_UTF8, &pSource); + + DxcBuffer sourceBuffer; + sourceBuffer.Ptr = pSource->GetBufferPointer(); + sourceBuffer.Size = pSource->GetBufferSize(); + sourceBuffer.Encoding = 0; + + IDxcResult* pCompileResult; + std::string error; + + HRESULT err = DxcInstances::Compiler->Compile(&sourceBuffer, arguments.data(), (uint32_t)arguments.size(), nullptr, IID_PPV_ARGS(&pCompileResult)); + + // Error Handling + const bool failed = FAILED(err); + if (failed) + error = std::format("Failed to compile, Error: {}\n", err); + IDxcBlobUtf8* pErrors; + pCompileResult->GetOutput(DXC_OUT_ERRORS, IID_PPV_ARGS(&pErrors), NULL); + if (pErrors && pErrors->GetStringLength() > 0) + error.append(std::format("{}\nWhile compiling shader file: {} \nAt stage: {}", (char*)pErrors->GetBufferPointer(), m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage))); + + if (error.empty()) + { + // Successful compilation + IDxcBlob* pResult; + pCompileResult->GetResult(&pResult); + + const size_t size = pResult->GetBufferSize(); + outputBinary.resize(size / sizeof(uint32_t)); + std::memcpy(outputBinary.data(), pResult->GetBufferPointer(), size); + pResult->Release(); + } + pCompileResult->Release(); + pSource->Release(); + + return error; +#elif defined(SE_PLATFORM_LINUX) + // Note(Emily): This is *atrocious* but dxc's integration refuses to process builtin HLSL without ICE'ing + // from the integration. + + char tempfileName[] = "starengine-hlsl-XXXXXX.spv"; + int outfile = mkstemps(tempfileName, 4); + + std::string dxc = std::format("{}/bin/dxc", FileSystem::GetEnvironmentVariable("VULKAN_SDK")); + std::string sourcePath = m_ShaderSourcePath.string(); + + std::vector exec{ + dxc.c_str(), + sourcePath.c_str(), + + "-E", "main", + "-T", ShaderUtils::HLSLShaderProfile(stage), + "-spirv", + "-fspv-target-env=vulkan1.2", + "-Zpc", + "-WX", + + "-I", "Resources/Shaders/Include/Common", + "-I", "Resources/Shaders/Include/HLSL", + + "-Fo", tempfileName + }; + + if (options.GenerateDebugInfo) + { + exec.push_back("-Qembed_debug"); + exec.push_back("-Zi"); + } + + if (stage & (VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT | VK_SHADER_STAGE_GEOMETRY_BIT)) + exec.push_back("-fvk-invert-y"); + + exec.push_back(NULL); + + // TODO(Emily): Error handling + pid_t pid; + posix_spawnattr_t attr; + posix_spawnattr_init(&attr); + + std::string ld_lib_path = std::format("LD_LIBRARY_PATH={}", getenv("LD_LIBRARY_PATH")); + char* env[] = { ld_lib_path.data(), NULL }; + if (posix_spawn(&pid, exec[0], NULL, &attr, (char**)exec.data(), env)) + { + return std::format("Could not execute `{}` for shader compilation: {} {}", exec[0], m_ShaderSourcePath.string(), ShaderUtils::ShaderStageToString(stage)); + } + int status; + waitpid(pid, &status, 0); + + if (WEXITSTATUS(status)) + { + return std::format("Compilation failed\nWhile compiling shader file: {} \nAt stage: {}", m_ShaderSourcePath.string(), ShaderUtils::ShaderStageToString(stage)); + } + + off_t size = lseek(outfile, 0, SEEK_END); + lseek(outfile, 0, SEEK_SET); + outputBinary.resize(size / sizeof(uint32_t)); + read(outfile, outputBinary.data(), size); + close(outfile); + unlink(tempfileName); + + return {}; +#endif + } + return "Unknown language!"; + } + + Ref VulkanShaderCompiler::Compile(const std::filesystem::path& shaderSourcePath, bool forceCompile, bool disableOptimization) + { + // Set name + std::string path = shaderSourcePath.string(); + size_t found = path.find_last_of("/\\"); + std::string name = found != std::string::npos ? path.substr(found + 1) : path; + found = name.find_last_of('.'); + name = found != std::string::npos ? name.substr(0, found) : name; + + Ref shader = Ref::Create(); + shader->m_AssetPath = shaderSourcePath; + shader->m_Name = name; + shader->m_DisableOptimization = disableOptimization; + + Ref compiler = Ref::Create(shaderSourcePath, disableOptimization); + compiler->Reload(forceCompile); + + shader->LoadAndCreateShaders(compiler->GetSPIRVData()); + shader->SetReflectionData(compiler->m_ReflectionData); + shader->CreateDescriptors(); + + Renderer::AcknowledgeParsedGlobalMacros(compiler->GetAcknowledgedMacros(), shader); + Renderer::OnShaderReloaded(shader->GetHash()); + return shader; + } + + bool VulkanShaderCompiler::TryRecompile(Ref shader) + { + Ref compiler = Ref::Create(shader->m_AssetPath, shader->m_DisableOptimization); + bool compileSucceeded = compiler->Reload(true); + if (!compileSucceeded) + return false; + + shader->Release(); + + shader->LoadAndCreateShaders(compiler->GetSPIRVData()); + shader->SetReflectionData(compiler->m_ReflectionData); + shader->CreateDescriptors(); + + Renderer::AcknowledgeParsedGlobalMacros(compiler->GetAcknowledgedMacros(), shader); + Renderer::OnShaderReloaded(shader->GetHash()); + + return true; + } + + bool VulkanShaderCompiler::CompileOrGetVulkanBinaries(std::map>& outputDebugBinary, std::map>& outputBinary, const nvrhi::ShaderType changedStages, const bool forceCompile) + { + for (auto [stage, source] : m_ShaderSource) + { + if (!CompileOrGetVulkanBinary(stage, outputDebugBinary[stage], true, changedStages, forceCompile)) + return false; + if (!CompileOrGetVulkanBinary(stage, outputBinary[stage], false, changedStages, forceCompile)) + return false; + } + return true; + } + + + bool VulkanShaderCompiler::CompileOrGetVulkanBinary(nvrhi::ShaderType stage, std::vector& outputBinary, bool debug, nvrhi::ShaderType changedStages, bool forceCompile) + { + const std::filesystem::path cacheDirectory = Utils::GetCacheDirectory(); + + // Compile shader with debug info so we can reflect + const auto extension = ShaderUtils::ShaderStageCachedFileExtension(stage, debug); + if (!forceCompile && (uint16_t)stage & ~(uint16_t)changedStages) // Per-stage cache is found and is unchanged + { + TryGetVulkanCachedBinary(cacheDirectory, extension, outputBinary); + } + + if (outputBinary.empty()) + { + CompilationOptions options; + if (debug) + { + options.GenerateDebugInfo = true; + options.Optimize = false; + } + else + { + options.GenerateDebugInfo = false; + // Disable optimization for compute shaders because of shaderc internal error + options.Optimize = !m_DisableOptimization && stage != VK_SHADER_STAGE_COMPUTE_BIT; + } + + if (std::string error = Compile(outputBinary, stage, options); error.size()) + { + SE_CORE_ERROR_TAG("Renderer", "{}", error); + TryGetVulkanCachedBinary(cacheDirectory, extension, outputBinary); + if (outputBinary.empty()) + { + SE_CONSOLE_LOG_ERROR("Failed to compile shader and couldn't find a cached version."); + } + else + { + SE_CONSOLE_LOG_ERROR("Failed to compile {}:{} so a cached version was loaded instead.", m_ShaderSourcePath.string(), nvrhi::utils::ShaderStageToString(stage)); +#if 0 + if (GImGui) // Guaranteed to be null before first ImGui frame + { + ImGuiWindow* logWindow = ImGui::FindWindowByName("Log"); + ImGui::FocusWindow(logWindow); + } +#endif + } + SE_CORE_ASSERT(false); + return false; + } + else // Compile success + { + auto path = cacheDirectory / (m_ShaderSourcePath.filename().string() + extension); + std::string cachedFilePath = path.string(); + + FILE* f = fopen(cachedFilePath.c_str(), "wb"); + if (!f) + SE_CORE_ERROR("Failed to cache shader binary!"); + fwrite(outputBinary.data(), sizeof(uint32_t), outputBinary.size(), f); + fclose(f); + } + } + + return true; + } + + void VulkanShaderCompiler::ClearReflectionData() + { + m_ReflectionData.ShaderDescriptorSets.clear(); + m_ReflectionData.Resources.clear(); + m_ReflectionData.ConstantBuffers.clear(); + m_ReflectionData.PushConstantRanges.clear(); + } + + void VulkanShaderCompiler::TryGetVulkanCachedBinary(const std::filesystem::path& cacheDirectory, const std::string& extension, std::vector& outputBinary) const + { + const auto path = cacheDirectory / (m_ShaderSourcePath.filename().string() + extension); + const std::string cachedFilePath = path.string(); + + FILE* f = fopen(cachedFilePath.data(), "rb"); + if (!f) + return; + + fseek(f, 0, SEEK_END); + uint64_t size = ftell(f); + fseek(f, 0, SEEK_SET); + outputBinary = std::vector(size / sizeof(uint32_t)); + fread(outputBinary.data(), sizeof(uint32_t), outputBinary.size(), f); + fclose(f); + } + + bool VulkanShaderCompiler::TryReadCachedReflectionData() + { + struct ReflectionFileHeader + { + char Header[4] = { 'H','Z','S','R' }; + } header; + + std::filesystem::path cacheDirectory = Utils::GetCacheDirectory(); + const auto path = cacheDirectory / (m_ShaderSourcePath.filename().string() + ".cached_vulkan.refl"); + FileStreamReader serializer(path); + if (!serializer) + return false; + + serializer.ReadRaw(header); + + bool validHeader = memcmp(&header, "HZSR", 4) == 0; + SE_CORE_VERIFY(validHeader); + if (!validHeader) + return false; + + ClearReflectionData(); + + uint32_t shaderDescriptorSetCount; + serializer.ReadRaw(shaderDescriptorSetCount); + + for (uint32_t i = 0; i < shaderDescriptorSetCount; i++) + { + auto& descriptorSet = m_ReflectionData.ShaderDescriptorSets.emplace_back(); + serializer.ReadMap(descriptorSet.UniformBuffers); + serializer.ReadMap(descriptorSet.StorageBuffers); + serializer.ReadMap(descriptorSet.ImageSamplers); + serializer.ReadMap(descriptorSet.StorageImages); + serializer.ReadMap(descriptorSet.SeparateTextures); + serializer.ReadMap(descriptorSet.SeparateSamplers); + serializer.ReadMap(descriptorSet.InputDeclarations); + } + + serializer.ReadMap(m_ReflectionData.Resources); + serializer.ReadMap(m_ReflectionData.ConstantBuffers); + serializer.ReadArray(m_ReflectionData.PushConstantRanges); + + return true; + } + + void VulkanShaderCompiler::SerializeReflectionData() + { + struct ReflectionFileHeader + { + char Header[4] = { 'H','Z','S','R' }; + } header; + + std::filesystem::path cacheDirectory = Utils::GetCacheDirectory(); + const auto path = cacheDirectory / (m_ShaderSourcePath.filename().string() + ".cached_vulkan.refl"); + FileStreamWriter serializer(path); + serializer.WriteRaw(header); + SerializeReflectionData(&serializer); + } + + void VulkanShaderCompiler::SerializeReflectionData(StreamWriter* serializer) + { + serializer->WriteRaw((uint32_t)m_ReflectionData.ShaderDescriptorSets.size()); + for (const auto& descriptorSet : m_ReflectionData.ShaderDescriptorSets) + { + serializer->WriteMap(descriptorSet.UniformBuffers); + serializer->WriteMap(descriptorSet.StorageBuffers); + serializer->WriteMap(descriptorSet.ImageSamplers); + serializer->WriteMap(descriptorSet.StorageImages); + serializer->WriteMap(descriptorSet.SeparateTextures); + serializer->WriteMap(descriptorSet.SeparateSamplers); + serializer->WriteMap(descriptorSet.InputDeclarations); + } + + serializer->WriteMap(m_ReflectionData.Resources); + serializer->WriteMap(m_ReflectionData.ConstantBuffers); + serializer->WriteArray(m_ReflectionData.PushConstantRanges); + } + + void VulkanShaderCompiler::ReflectAllShaderStages(const std::map>& shaderData) + { + ClearReflectionData(); + + for (auto [stage, data] : shaderData) + { + Reflect(stage, data); + } + } + + void VulkanShaderCompiler::Reflect(nvrhi::ShaderType shaderStage, const std::vector& shaderData) + { + SE_CORE_INFO_TAG("Renderer", "==========================="); + SE_CORE_INFO_TAG("Renderer", " Vulkan Shader Reflection"); + SE_CORE_INFO_TAG("Renderer", "==========================="); + SE_CORE_INFO_TAG("Renderer", m_ShaderSourcePath.string()); + + spirv_cross::Compiler compiler(shaderData); + auto resources = compiler.get_shader_resources(); + + SE_CORE_INFO_TAG("Renderer", "Uniform Buffers:"); + for (const auto& resource : resources.uniform_buffers) + { + auto activeBuffers = compiler.get_active_buffer_ranges(resource.id); + // Discard unused buffers from headers + if (activeBuffers.size()) + { + const auto& name = resource.name; + auto& bufferType = compiler.get_type(resource.base_type_id); + int memberCount = (uint32_t)bufferType.member_types.size(); + uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding); + uint32_t descriptorSet = compiler.get_decoration(resource.id, spv::DecorationDescriptorSet); + uint32_t size = (uint32_t)compiler.get_declared_struct_size(bufferType); + + if (descriptorSet >= m_ReflectionData.ShaderDescriptorSets.size()) + m_ReflectionData.ShaderDescriptorSets.resize(descriptorSet + 1); + + ShaderResource::ShaderDescriptorSet& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[descriptorSet]; + if (s_UniformBuffers[descriptorSet].find(binding) == s_UniformBuffers[descriptorSet].end()) + { + ShaderResource::UniformBuffer uniformBuffer; + uniformBuffer.BindingPoint = binding; + uniformBuffer.Size = size; + uniformBuffer.Name = name; + uniformBuffer.ShaderStage = nvrhi::ShaderType::All; + s_UniformBuffers.at(descriptorSet)[binding] = uniformBuffer; + } + else + { + ShaderResource::UniformBuffer& uniformBuffer = s_UniformBuffers.at(descriptorSet).at(binding); + if (size > uniformBuffer.Size) + uniformBuffer.Size = size; + } + shaderDescriptorSet.UniformBuffers[binding] = s_UniformBuffers.at(descriptorSet).at(binding); + + SE_CORE_INFO_TAG("Renderer", " {0} ({1}, {2})", name, descriptorSet, binding); + SE_CORE_INFO_TAG("Renderer", " Member Count: {0}", memberCount); + SE_CORE_INFO_TAG("Renderer", " Size: {0}", size); + SE_CORE_INFO_TAG("Renderer", "-------------------"); + } + } + + SE_CORE_INFO_TAG("Renderer", "Storage Buffers:"); + for (const auto& resource : resources.storage_buffers) + { + auto activeBuffers = compiler.get_active_buffer_ranges(resource.id); + // Discard unused buffers from headers + if (activeBuffers.size()) + { + const auto& name = resource.name; + auto& bufferType = compiler.get_type(resource.base_type_id); + uint32_t memberCount = (uint32_t)bufferType.member_types.size(); + uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding); + uint32_t descriptorSet = compiler.get_decoration(resource.id, spv::DecorationDescriptorSet); + uint32_t size = (uint32_t)compiler.get_declared_struct_size(bufferType); + + if (descriptorSet >= m_ReflectionData.ShaderDescriptorSets.size()) + m_ReflectionData.ShaderDescriptorSets.resize(descriptorSet + 1); + + ShaderResource::ShaderDescriptorSet& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[descriptorSet]; + if (s_StorageBuffers[descriptorSet].find(binding) == s_StorageBuffers[descriptorSet].end()) + { + ShaderResource::StorageBuffer storageBuffer; + storageBuffer.BindingPoint = binding; + storageBuffer.Size = size; + storageBuffer.Name = name; + storageBuffer.ShaderStage = nvrhi::ShaderType::All; + s_StorageBuffers.at(descriptorSet)[binding] = storageBuffer; + } + else + { + ShaderResource::StorageBuffer& storageBuffer = s_StorageBuffers.at(descriptorSet).at(binding); + if (size > storageBuffer.Size) + storageBuffer.Size = size; + } + + shaderDescriptorSet.StorageBuffers[binding] = s_StorageBuffers.at(descriptorSet).at(binding); + + SE_CORE_INFO_TAG("Renderer", " {0} ({1}, {2})", name, descriptorSet, binding); + SE_CORE_INFO_TAG("Renderer", " Member Count: {0}", memberCount); + SE_CORE_INFO_TAG("Renderer", " Size: {0}", size); + SE_CORE_INFO_TAG("Renderer", "-------------------"); + } + } + + SE_CORE_INFO_TAG("Renderer", "Push Constant Buffers:"); + for (const auto& resource : resources.push_constant_buffers) + { + const auto& bufferName = resource.name; + auto& bufferType = compiler.get_type(resource.base_type_id); + auto bufferSize = (uint32_t)compiler.get_declared_struct_size(bufferType); + uint32_t memberCount = uint32_t(bufferType.member_types.size()); + uint32_t bufferOffset = 0; + if (m_ReflectionData.PushConstantRanges.size()) + bufferOffset = m_ReflectionData.PushConstantRanges.back().Offset + m_ReflectionData.PushConstantRanges.back().Size; + + auto& pushConstantRange = m_ReflectionData.PushConstantRanges.emplace_back(); + pushConstantRange.ShaderStage = shaderStage; + pushConstantRange.Size = bufferSize; + pushConstantRange.Offset = 0;// bufferOffset; + + // Skip empty push constant buffers - these are for the renderer only + if (bufferName.empty() || bufferName == "u_Renderer") + continue; + + ShaderBuffer& buffer = m_ReflectionData.ConstantBuffers[bufferName]; + buffer.Name = bufferName; + buffer.Size = bufferSize; + + SE_CORE_INFO_TAG("Renderer", " Name: {0}", bufferName); + SE_CORE_INFO_TAG("Renderer", " Member Count: {0}", memberCount); + SE_CORE_INFO_TAG("Renderer", " Size: {0}", bufferSize); + + for (uint32_t i = 0; i < memberCount; i++) + { + auto type = compiler.get_type(bufferType.member_types[i]); + const auto& memberName = compiler.get_member_name(bufferType.self, i); + auto size = (uint32_t)compiler.get_declared_struct_member_size(bufferType, i); + auto offset = compiler.type_struct_member_offset(bufferType, i); + + std::string uniformName = std::format("{}.{}", bufferName, memberName); + buffer.Uniforms[uniformName] = ShaderUniform(uniformName, Utils::SPIRTypeToShaderUniformType(type), size, offset); + } + } + + SE_CORE_INFO_TAG("Renderer", "Sampled Images:"); + for (const auto& resource : resources.sampled_images) + { + const auto& name = resource.name; + auto& baseType = compiler.get_type(resource.base_type_id); + auto& type = compiler.get_type(resource.type_id); + uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding); + uint32_t descriptorSet = compiler.get_decoration(resource.id, spv::DecorationDescriptorSet); + uint32_t dimension = 0; + switch (baseType.image.dim) + { + case spv::Dim::Dim1D: + dimension = 1; + break; + case spv::Dim::Dim2D: + dimension = 2; + break; + case spv::Dim::Dim3D: + case spv::Dim::DimCube: + dimension = 3; + break; + } + uint32_t arraySize = type.array.size() > 0 ? type.array[0] : 1; + if (arraySize == 0) + arraySize = 1; + if (descriptorSet >= m_ReflectionData.ShaderDescriptorSets.size()) + m_ReflectionData.ShaderDescriptorSets.resize(descriptorSet + 1); + + ShaderResource::ShaderDescriptorSet& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[descriptorSet]; + auto& imageSampler = shaderDescriptorSet.ImageSamplers[binding]; + imageSampler.BindingPoint = binding; + imageSampler.DescriptorSet = descriptorSet; + imageSampler.Name = name; + imageSampler.ShaderStage = shaderStage; + imageSampler.Dimension = dimension; + imageSampler.ArraySize = arraySize; + + m_ReflectionData.Resources[name] = ShaderResourceDeclaration(name, descriptorSet, binding, arraySize); + + SE_CORE_INFO_TAG("Renderer", " {0} ({1}, {2})", name, descriptorSet, binding); + } + + SE_CORE_INFO_TAG("Renderer", "Separate Images:"); + for (const auto& resource : resources.separate_images) + { + const auto& name = resource.name; + auto& baseType = compiler.get_type(resource.base_type_id); + auto& type = compiler.get_type(resource.type_id); + uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding); + uint32_t descriptorSet = compiler.get_decoration(resource.id, spv::DecorationDescriptorSet); + uint32_t dimension = 0; + switch (baseType.image.dim) + { + case spv::Dim::Dim1D: + dimension = 1; + break; + case spv::Dim::Dim2D: + dimension = 2; + break; + case spv::Dim::Dim3D: + case spv::Dim::DimCube: + dimension = 3; + break; + } + uint32_t arraySize = type.array.size() > 0 ? type.array[0] : 1; + if (arraySize == 0) + arraySize = 1; + if (descriptorSet >= m_ReflectionData.ShaderDescriptorSets.size()) + m_ReflectionData.ShaderDescriptorSets.resize(descriptorSet + 1); + + ShaderResource::ShaderDescriptorSet& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[descriptorSet]; + auto& imageSampler = shaderDescriptorSet.SeparateTextures[binding]; + imageSampler.BindingPoint = binding; + imageSampler.DescriptorSet = descriptorSet; + imageSampler.Name = name; + imageSampler.ShaderStage = shaderStage; + imageSampler.Dimension = dimension; + imageSampler.ArraySize = arraySize; + + m_ReflectionData.Resources[name] = ShaderResourceDeclaration(name, descriptorSet, binding, arraySize); + + SE_CORE_INFO_TAG("Renderer", " {0} ({1}, {2})", name, descriptorSet, binding); + } + + SE_CORE_INFO_TAG("Renderer", "Separate Samplers:"); + for (const auto& resource : resources.separate_samplers) + { + const auto& name = resource.name; + auto& baseType = compiler.get_type(resource.base_type_id); + auto& type = compiler.get_type(resource.type_id); + uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding); + uint32_t descriptorSet = compiler.get_decoration(resource.id, spv::DecorationDescriptorSet); + uint32_t dimension = 0; +#if 0 + switch (baseType.image.dim) + { + case spv::Dim::Dim1D: + dimension = 1; + break; + case spv::Dim::Dim2D: + dimension = 2; + break; + case spv::Dim::Dim3D: + case spv::Dim::DimCube: + dimension = 3; + break; + } +#endif + uint32_t arraySize = type.array.size() > 0 ? type.array[0] : 1; + if (arraySize == 0) + arraySize = 1; + if (descriptorSet >= m_ReflectionData.ShaderDescriptorSets.size()) + m_ReflectionData.ShaderDescriptorSets.resize(descriptorSet + 1); + + ShaderResource::ShaderDescriptorSet& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[descriptorSet]; + auto& imageSampler = shaderDescriptorSet.SeparateSamplers[binding]; + imageSampler.BindingPoint = binding; + imageSampler.DescriptorSet = descriptorSet; + imageSampler.Name = name; + imageSampler.ShaderStage = shaderStage; + imageSampler.Dimension = dimension; + imageSampler.ArraySize = arraySize; + + m_ReflectionData.Resources[name] = ShaderResourceDeclaration(name, descriptorSet, binding, arraySize); + + SE_CORE_INFO_TAG("Renderer", " {0} ({1}, {2})", name, descriptorSet, binding); + } + + SE_CORE_INFO_TAG("Renderer", "Storage Images:"); + for (const auto& resource : resources.storage_images) + { + const auto& name = resource.name; + auto& type = compiler.get_type(resource.type_id); + uint32_t binding = compiler.get_decoration(resource.id, spv::DecorationBinding); + uint32_t descriptorSet = compiler.get_decoration(resource.id, spv::DecorationDescriptorSet); + uint32_t dimension = 0; + switch (type.image.dim) + { + case spv::Dim::Dim1D: + dimension = 1; + break; + case spv::Dim::Dim2D: + dimension = 2; + break; + case spv::Dim::Dim3D: + case spv::Dim::DimCube: + dimension = 3; + break; + } + uint32_t arraySize = type.array.size() > 0 ? type.array[0] : 1; + if (arraySize == 0) + arraySize = 1; + if (descriptorSet >= m_ReflectionData.ShaderDescriptorSets.size()) + m_ReflectionData.ShaderDescriptorSets.resize(descriptorSet + 1); + + ShaderResource::ShaderDescriptorSet& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[descriptorSet]; + auto& imageSampler = shaderDescriptorSet.StorageImages[binding]; + imageSampler.BindingPoint = binding; + imageSampler.DescriptorSet = descriptorSet; + imageSampler.Name = name; + imageSampler.Dimension = dimension; + imageSampler.ArraySize = arraySize; + imageSampler.ShaderStage = shaderStage; + + m_ReflectionData.Resources[name] = ShaderResourceDeclaration(name, descriptorSet, binding, arraySize); + + SE_CORE_INFO_TAG("Renderer", " {0} ({1}, {2})", name, descriptorSet, binding); + } + + SE_CORE_INFO_TAG("Renderer", "Special macros:"); + for (const auto& macro : m_AcknowledgedMacros) + { + SE_CORE_INFO_TAG("Renderer", " {0}", macro); + } + + SE_CORE_INFO_TAG("Renderer", "==========================="); + + + } + + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.h b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.h new file mode 100644 index 00000000..e1e29082 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/ShaderCompiler/VulkanShaderCompiler.h @@ -0,0 +1,98 @@ +#pragma once + +#include "vulkan/vulkan.h" + +#include "nvrhi/nvrhi.h" + +#include "StarEngine/Platform/Vulkan/VulkanShaderResource.h" +#include "StarEngine/Platform/Vulkan/VulkanShaderUtils.h" +#include "StarEngine/Platform/Vulkan/VulkanShader.h" + +#include "ShaderPreprocessing/ShaderPreprocessor.h" + +#include +#include +#include +#include + +struct IDxcCompiler3; +struct IDxcUtils; + +namespace StarEngine { + + struct DxcInstances + { + inline static IDxcCompiler3* Compiler = nullptr; + inline static IDxcUtils* Utils = nullptr; + }; + struct StageData + { + std::unordered_set Headers; + uint32_t HashValue = 0; + bool operator== (const StageData& other) const noexcept { return this->Headers == other.Headers && this->HashValue == other.HashValue; } + bool operator!= (const StageData& other) const noexcept { return !(*this == other); } + }; + + class VulkanShader; + + class VulkanShaderCompiler : public RefCounted + { + public: + VulkanShaderCompiler(const std::filesystem::path& shaderSourcePath, bool disableOptimization = false); + + bool Reload(bool forceCompile = false); + + const std::map>& GetSPIRVData() const { return m_SPIRVData; } + const std::unordered_set& GetAcknowledgedMacros() const { return m_AcknowledgedMacros; } + + static void ClearUniformBuffers(); + + static Ref Compile(const std::filesystem::path& shaderSourcePath, bool forceCompile = false, bool disableOptimization = false); + static bool TryRecompile(Ref shader); + private: + std::map PreProcess(const std::string& source); + std::map PreProcessGLSL(const std::string& source); + std::map PreProcessHLSL(const std::string& source); + + struct CompilationOptions + { + bool GenerateDebugInfo = false; + bool Optimize = true; + }; + + std::string Compile(std::vector& outputBinary, const nvrhi::ShaderType stage, CompilationOptions options) const; + bool CompileOrGetVulkanBinaries(std::map>& outputDebugBinary, std::map>& outputBinary, const nvrhi::ShaderType changedStages, const bool forceCompile); + bool CompileOrGetVulkanBinary(nvrhi::ShaderType stage, std::vector& outputBinary, bool debug, nvrhi::ShaderType changedStages, bool forceCompile); + + void ClearReflectionData(); + + void TryGetVulkanCachedBinary(const std::filesystem::path& cacheDirectory, const std::string& extension, std::vector& outputBinary) const; + bool TryReadCachedReflectionData(); + void SerializeReflectionData(); + void SerializeReflectionData(StreamWriter* serializer); + + void ReflectAllShaderStages(const std::map>& shaderData); + void Reflect(nvrhi::ShaderType shaderStage, const std::vector& shaderData); + private: + std::filesystem::path m_ShaderSourcePath; + bool m_DisableOptimization = false; + + std::map m_ShaderSource; + std::map> m_SPIRVDebugData, m_SPIRVData; + + // Reflection info + VulkanShader::ReflectionData m_ReflectionData; + + // Names of macros that are parsed from shader. + // These are used to reliably get informattion about what shaders need what macros + std::unordered_set m_AcknowledgedMacros; + ShaderUtils::SourceLang m_Language; + + std::map m_StagesMetadata; + + friend class VulkanShader; + friend class VulkanShaderCache; + friend class ShaderPack; + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/Vulkan.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/Vulkan.cpp new file mode 100644 index 00000000..22d1a52b --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/Vulkan.cpp @@ -0,0 +1,111 @@ +#include "sepch.h" +#include "Vulkan.h" + +#include "VulkanContext.h" +#include "VulkanDiagnostics.h" + +namespace StarEngine::Utils { + + void VulkanLoadDebugUtilsExtensions(VkInstance instance) + { + fpSetDebugUtilsObjectNameEXT = (PFN_vkSetDebugUtilsObjectNameEXT)(vkGetInstanceProcAddr(instance, "vkSetDebugUtilsObjectNameEXT")); + if (fpSetDebugUtilsObjectNameEXT == nullptr) + fpSetDebugUtilsObjectNameEXT = [](VkDevice device, const VkDebugUtilsObjectNameInfoEXT* pNameInfo) { return VK_SUCCESS; }; + + fpCmdBeginDebugUtilsLabelEXT = (PFN_vkCmdBeginDebugUtilsLabelEXT)(vkGetInstanceProcAddr(instance, "vkCmdBeginDebugUtilsLabelEXT")); + if (fpCmdBeginDebugUtilsLabelEXT == nullptr) + fpCmdBeginDebugUtilsLabelEXT = [](VkCommandBuffer commandBuffer, const VkDebugUtilsLabelEXT* pLabelInfo) {}; + + fpCmdEndDebugUtilsLabelEXT = (PFN_vkCmdEndDebugUtilsLabelEXT)(vkGetInstanceProcAddr(instance, "vkCmdEndDebugUtilsLabelEXT")); + if (fpCmdEndDebugUtilsLabelEXT == nullptr) + fpCmdEndDebugUtilsLabelEXT = [](VkCommandBuffer commandBuffer) {}; + + fpCmdInsertDebugUtilsLabelEXT = (PFN_vkCmdInsertDebugUtilsLabelEXT)(vkGetInstanceProcAddr(instance, "vkCmdInsertDebugUtilsLabelEXT")); + if (fpCmdInsertDebugUtilsLabelEXT == nullptr) + fpCmdInsertDebugUtilsLabelEXT = [](VkCommandBuffer commandBuffer, const VkDebugUtilsLabelEXT* pLabelInfo) {}; + } + + static const char* StageToString(VkPipelineStageFlagBits stage) + { + switch (stage) + { + case VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT: return "VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT"; + case VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT: return "VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT"; + } + SE_CORE_ASSERT(false); + return nullptr; + } + + void RetrieveDiagnosticCheckpoints() + { + bool supported = VulkanContext::GetCurrentDevice()->GetPhysicalDevice()->IsExtensionSupported(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); + if (!supported) + return; + + + { + const uint32_t checkpointCount = 4; + VkCheckpointDataNV data[checkpointCount]; + for (uint32_t i = 0; i < checkpointCount; i++) + data[i].sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV; + + uint32_t retrievedCount = checkpointCount; + vkGetQueueCheckpointDataNV(::StarEngine::VulkanContext::GetCurrentDevice()->GetGraphicsQueue(), &retrievedCount, data); + SE_CORE_ERROR("RetrieveDiagnosticCheckpoints (Graphics Queue):"); + for (uint32_t i = 0; i < retrievedCount; i++) + { + VulkanCheckpointData* checkpoint = (VulkanCheckpointData*)data[i].pCheckpointMarker; + SE_CORE_ERROR("Checkpoint: {0} (stage: {1})", checkpoint->Data, StageToString(data[i].stage)); + } + } + { + const uint32_t checkpointCount = 4; + VkCheckpointDataNV data[checkpointCount]; + for (uint32_t i = 0; i < checkpointCount; i++) + data[i].sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV; + + uint32_t retrievedCount = checkpointCount; + vkGetQueueCheckpointDataNV(::StarEngine::VulkanContext::GetCurrentDevice()->GetComputeQueue(), &retrievedCount, data); + SE_CORE_ERROR("RetrieveDiagnosticCheckpoints (Compute Queue):"); + for (uint32_t i = 0; i < retrievedCount; i++) + { + VulkanCheckpointData* checkpoint = (VulkanCheckpointData*)data[i].pCheckpointMarker; + SE_CORE_ERROR("Checkpoint: {0} (stage: {1})", checkpoint->Data, StageToString(data[i].stage)); + } + } + //__debugbreak(); + } + + void VulkanCheckResult(VkResult result) + { + if (result != VK_SUCCESS) + { + SE_CORE_ERROR("VkResult is '{0}'", Utils::VKResultToString(result)); + if (result == VK_ERROR_DEVICE_LOST) + { + using namespace std::chrono_literals; + std::this_thread::sleep_for(3s); + //Utils::RetrieveDiagnosticCheckpoints(); + Utils::DumpGPUInfo(); + } + SE_CORE_ASSERT(result == VK_SUCCESS); + } + } + + void VulkanCheckResult(VkResult result, const char* file, int line) + { + if (result != VK_SUCCESS) + { + SE_CORE_ERROR("VkResult is '{0}' in {1}:{2}", Utils::VKResultToString(result), file, line); + if (result == VK_ERROR_DEVICE_LOST) + { + using namespace std::chrono_literals; + std::this_thread::sleep_for(3s); + //Utils::RetrieveDiagnosticCheckpoints(); + Utils::DumpGPUInfo(); + } + SE_CORE_ASSERT(result == VK_SUCCESS); + } + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/Vulkan.h b/StarEngine/src/StarEngine/Platform/Vulkan/Vulkan.h new file mode 100644 index 00000000..5c2e0e1d --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/Vulkan.h @@ -0,0 +1,137 @@ +#pragma once + +#include + +inline PFN_vkSetDebugUtilsObjectNameEXT fpSetDebugUtilsObjectNameEXT; //Making it static randomly sets it to nullptr for some reason. +inline PFN_vkCmdBeginDebugUtilsLabelEXT fpCmdBeginDebugUtilsLabelEXT; +inline PFN_vkCmdEndDebugUtilsLabelEXT fpCmdEndDebugUtilsLabelEXT; +inline PFN_vkCmdInsertDebugUtilsLabelEXT fpCmdInsertDebugUtilsLabelEXT; + +namespace StarEngine::Utils { + void VulkanLoadDebugUtilsExtensions(VkInstance instance); + + inline const char* VKResultToString(VkResult result) + { + switch (result) + { + case VK_SUCCESS: return "VK_SUCCESS"; + case VK_NOT_READY: return "VK_NOT_READY"; + case VK_TIMEOUT: return "VK_TIMEOUT"; + case VK_EVENT_SET: return "VK_EVENT_SET"; + case VK_EVENT_RESET: return "VK_EVENT_RESET"; + case VK_INCOMPLETE: return "VK_INCOMPLETE"; + case VK_ERROR_OUT_OF_HOST_MEMORY: return "VK_ERROR_OUT_OF_HOST_MEMORY"; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: return "VK_ERROR_OUT_OF_DEVICE_MEMORY"; + case VK_ERROR_INITIALIZATION_FAILED: return "VK_ERROR_INITIALIZATION_FAILED"; + case VK_ERROR_DEVICE_LOST: return "VK_ERROR_DEVICE_LOST"; + case VK_ERROR_MEMORY_MAP_FAILED: return "VK_ERROR_MEMORY_MAP_FAILED"; + case VK_ERROR_LAYER_NOT_PRESENT: return "VK_ERROR_LAYER_NOT_PRESENT"; + case VK_ERROR_EXTENSION_NOT_PRESENT: return "VK_ERROR_EXTENSION_NOT_PRESENT"; + case VK_ERROR_FEATURE_NOT_PRESENT: return "VK_ERROR_FEATURE_NOT_PRESENT"; + case VK_ERROR_INCOMPATIBLE_DRIVER: return "VK_ERROR_INCOMPATIBLE_DRIVER"; + case VK_ERROR_TOO_MANY_OBJECTS: return "VK_ERROR_TOO_MANY_OBJECTS"; + case VK_ERROR_FORMAT_NOT_SUPPORTED: return "VK_ERROR_FORMAT_NOT_SUPPORTED"; + case VK_ERROR_FRAGMENTED_POOL: return "VK_ERROR_FRAGMENTED_POOL"; + case VK_ERROR_UNKNOWN: return "VK_ERROR_UNKNOWN"; + case VK_ERROR_OUT_OF_POOL_MEMORY: return "VK_ERROR_OUT_OF_POOL_MEMORY"; + case VK_ERROR_INVALID_EXTERNAL_HANDLE: return "VK_ERROR_INVALID_EXTERNAL_HANDLE"; + case VK_ERROR_FRAGMENTATION: return "VK_ERROR_FRAGMENTATION"; + case VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS: return "VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS"; + case VK_ERROR_SURFACE_LOST_KHR: return "VK_ERROR_SURFACE_LOST_KHR"; + case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: return "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR"; + case VK_SUBOPTIMAL_KHR: return "VK_SUBOPTIMAL_KHR"; + case VK_ERROR_OUT_OF_DATE_KHR: return "VK_ERROR_OUT_OF_DATE_KHR"; + case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR: return "VK_ERROR_INCOMPATIBLE_DISPLAY_KHR"; + case VK_ERROR_VALIDATION_FAILED_EXT: return "VK_ERROR_VALIDATION_FAILED_EXT"; + case VK_ERROR_INVALID_SHADER_NV: return "VK_ERROR_INVALID_SHADER_NV"; + case VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT: return "VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT"; + case VK_ERROR_NOT_PERMITTED_EXT: return "VK_ERROR_NOT_PERMITTED_EXT"; + case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT: return "VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT"; + case VK_THREAD_IDLE_KHR: return "VK_THREAD_IDLE_KHR"; + case VK_THREAD_DONE_KHR: return "VK_THREAD_DONE_KHR"; + case VK_OPERATION_DEFERRED_KHR: return "VK_OPERATION_DEFERRED_KHR"; + case VK_OPERATION_NOT_DEFERRED_KHR: return "VK_OPERATION_NOT_DEFERRED_KHR"; + case VK_PIPELINE_COMPILE_REQUIRED_EXT: return "VK_PIPELINE_COMPILE_REQUIRED_EXT"; + } + SE_CORE_ASSERT(false); + return nullptr; + } + + inline const char* VkObjectTypeToString(const VkObjectType type) + { + switch (type) + { + case VK_OBJECT_TYPE_COMMAND_BUFFER: return "VK_OBJECT_TYPE_COMMAND_BUFFER"; + case VK_OBJECT_TYPE_PIPELINE: return "VK_OBJECT_TYPE_PIPELINE"; + case VK_OBJECT_TYPE_FRAMEBUFFER: return "VK_OBJECT_TYPE_FRAMEBUFFER"; + case VK_OBJECT_TYPE_IMAGE: return "VK_OBJECT_TYPE_IMAGE"; + case VK_OBJECT_TYPE_QUERY_POOL: return "VK_OBJECT_TYPE_QUERY_POOL"; + case VK_OBJECT_TYPE_RENDER_PASS: return "VK_OBJECT_TYPE_RENDER_PASS"; + case VK_OBJECT_TYPE_COMMAND_POOL: return "VK_OBJECT_TYPE_COMMAND_POOL"; + case VK_OBJECT_TYPE_PIPELINE_CACHE: return "VK_OBJECT_TYPE_PIPELINE_CACHE"; + case VK_OBJECT_TYPE_ACCELERATION_STRUCTURE_KHR: return "VK_OBJECT_TYPE_ACCELERATION_STRUCTURE_KHR"; + case VK_OBJECT_TYPE_ACCELERATION_STRUCTURE_NV: return "VK_OBJECT_TYPE_ACCELERATION_STRUCTURE_NV"; + case VK_OBJECT_TYPE_BUFFER: return "VK_OBJECT_TYPE_BUFFER"; + case VK_OBJECT_TYPE_BUFFER_VIEW: return "VK_OBJECT_TYPE_BUFFER_VIEW"; + case VK_OBJECT_TYPE_DEBUG_REPORT_CALLBACK_EXT: return "VK_OBJECT_TYPE_DEBUG_REPORT_CALLBACK_EXT"; + case VK_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT: return "VK_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT"; + case VK_OBJECT_TYPE_DEFERRED_OPERATION_KHR: return "VK_OBJECT_TYPE_DEFERRED_OPERATION_KHR"; + case VK_OBJECT_TYPE_DESCRIPTOR_POOL: return "VK_OBJECT_TYPE_DESCRIPTOR_POOL"; + case VK_OBJECT_TYPE_DESCRIPTOR_SET: return "VK_OBJECT_TYPE_DESCRIPTOR_SET"; + case VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT: return "VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT"; + case VK_OBJECT_TYPE_DESCRIPTOR_UPDATE_TEMPLATE: return "VK_OBJECT_TYPE_DESCRIPTOR_UPDATE_TEMPLATE"; + case VK_OBJECT_TYPE_PRIVATE_DATA_SLOT_EXT: return "VK_OBJECT_TYPE_PRIVATE_DATA_SLOT_EXT"; + case VK_OBJECT_TYPE_DEVICE: return "VK_OBJECT_TYPE_DEVICE"; + case VK_OBJECT_TYPE_DEVICE_MEMORY: return "VK_OBJECT_TYPE_DEVICE_MEMORY"; + case VK_OBJECT_TYPE_PIPELINE_LAYOUT: return "VK_OBJECT_TYPE_PIPELINE_LAYOUT"; + case VK_OBJECT_TYPE_DISPLAY_KHR: return "VK_OBJECT_TYPE_DISPLAY_KHR"; + case VK_OBJECT_TYPE_DISPLAY_MODE_KHR: return "VK_OBJECT_TYPE_DISPLAY_MODE_KHR"; + case VK_OBJECT_TYPE_PHYSICAL_DEVICE: return "VK_OBJECT_TYPE_PHYSICAL_DEVICE"; + case VK_OBJECT_TYPE_EVENT: return "VK_OBJECT_TYPE_EVENT"; + case VK_OBJECT_TYPE_FENCE: return "VK_OBJECT_TYPE_FENCE"; + case VK_OBJECT_TYPE_IMAGE_VIEW: return "VK_OBJECT_TYPE_IMAGE_VIEW"; + case VK_OBJECT_TYPE_INDIRECT_COMMANDS_LAYOUT_NV: return "VK_OBJECT_TYPE_INDIRECT_COMMANDS_LAYOUT_NV"; + case VK_OBJECT_TYPE_INSTANCE: return "VK_OBJECT_TYPE_INSTANCE"; + case VK_OBJECT_TYPE_PERFORMANCE_CONFIGURATION_INTEL: return "VK_OBJECT_TYPE_PERFORMANCE_CONFIGURATION_INTEL"; + case VK_OBJECT_TYPE_QUEUE: return "VK_OBJECT_TYPE_QUEUE"; + case VK_OBJECT_TYPE_SAMPLER: return "VK_OBJECT_TYPE_SAMPLER"; + case VK_OBJECT_TYPE_SAMPLER_YCBCR_CONVERSION: return "VK_OBJECT_TYPE_SAMPLER_YCBCR_CONVERSION"; + case VK_OBJECT_TYPE_SEMAPHORE: return "VK_OBJECT_TYPE_SEMAPHORE"; + case VK_OBJECT_TYPE_SHADER_MODULE: return "VK_OBJECT_TYPE_SHADER_MODULE"; + case VK_OBJECT_TYPE_SURFACE_KHR: return "VK_OBJECT_TYPE_SURFACE_KHR"; + case VK_OBJECT_TYPE_SWAPCHAIN_KHR: return "VK_OBJECT_TYPE_SWAPCHAIN_KHR"; + case VK_OBJECT_TYPE_VALIDATION_CACHE_EXT: return "VK_OBJECT_TYPE_VALIDATION_CACHE_EXT"; + case VK_OBJECT_TYPE_UNKNOWN: return "VK_OBJECT_TYPE_UNKNOWN"; + case VK_OBJECT_TYPE_MAX_ENUM: return "VK_OBJECT_TYPE_MAX_ENUM"; + } + SE_CORE_ASSERT(false); + return ""; + } + + void RetrieveDiagnosticCheckpoints(); + + void VulkanCheckResult(VkResult result); + + void VulkanCheckResult(VkResult result, const char* file, int line); +} + +#define VK_CHECK_RESULT(f)\ +{\ + VkResult res = (f);\ + ::StarEngine::Utils::VulkanCheckResult(res, __FILE__, __LINE__);\ +} + +namespace StarEngine::VKUtils +{ + inline static void SetDebugUtilsObjectName(const VkDevice device, const VkObjectType objectType, const std::string& name, const void* handle) + { + VkDebugUtilsObjectNameInfoEXT nameInfo; + nameInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT; + nameInfo.objectType = objectType; + nameInfo.pObjectName = name.c_str(); + nameInfo.objectHandle = (uint64_t)handle; + nameInfo.pNext = VK_NULL_HANDLE; + + VK_CHECK_RESULT(fpSetDebugUtilsObjectNameEXT(device, &nameInfo)); + } +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAPI.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAPI.cpp new file mode 100644 index 00000000..7a166004 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAPI.cpp @@ -0,0 +1,42 @@ +#include "sepch.h" +#include "VulkanAPI.h" + +#include "VulkanContext.h" + +#include "StarEngine/Renderer/RendererStats.h" + +namespace StarEngine::Vulkan { + + VkDescriptorSetAllocateInfo DescriptorSetAllocInfo(const VkDescriptorSetLayout* layouts, uint32_t count, VkDescriptorPool pool) + { + VkDescriptorSetAllocateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + info.pSetLayouts = layouts; + info.descriptorSetCount = count; + info.descriptorPool = pool; + return info; + } + + VkSampler CreateSampler(VkSamplerCreateInfo samplerCreateInfo) + { + auto device = VulkanContext::GetCurrentDevice(); + VkDevice vulkanDevice = device->GetVulkanDevice(); + + VkSampler sampler; + VK_CHECK_RESULT(vkCreateSampler(vulkanDevice, &samplerCreateInfo, nullptr, &sampler)); + + RendererUtils::GetResourceAllocationCounts().Samplers++; + + return sampler; + } + + void DestroySampler(VkSampler sampler) + { + auto device = VulkanContext::GetCurrentDevice(); + VkDevice vulkanDevice = device->GetVulkanDevice(); + vkDestroySampler(vulkanDevice, sampler, nullptr); + + RendererUtils::GetResourceAllocationCounts().Samplers--; + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAPI.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAPI.h new file mode 100644 index 00000000..ddd7a792 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAPI.h @@ -0,0 +1,12 @@ +#pragma once + +#include "vulkan/vulkan.h" + +namespace StarEngine::Vulkan { + + VkDescriptorSetAllocateInfo DescriptorSetAllocInfo(const VkDescriptorSetLayout* layouts, uint32_t count = 1, VkDescriptorPool pool = nullptr); + + VkSampler CreateSampler(VkSamplerCreateInfo samplerCreateInfo); + void DestroySampler(VkSampler sampler); + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAllocator.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAllocator.cpp new file mode 100644 index 00000000..2e6785fc --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAllocator.cpp @@ -0,0 +1,293 @@ +#include "sepch.h" +#include "VulkanAllocator.h" + +#include "VulkanContext.h" + +#include "StarEngine/Utilities/StringUtils.h" + +#if SE_LOG_RENDERER_ALLOCATIONS +#define SE_ALLOCATOR_LOG(...) SE_CORE_TRACE(__VA_ARGS__) +#else +#define SE_ALLOCATOR_LOG(...) +#endif + +#define SE_GPU_TRACK_MEMORY_ALLOCATION 1 + +namespace StarEngine { + + struct VulkanAllocatorData + { + VmaAllocator Allocator; + uint64_t TotalAllocatedBytes = 0; + + uint64_t MemoryUsage = 0; // all heaps + }; + + enum class AllocationType : uint8_t + { + None = 0, Buffer = 1, Image = 2 + }; + + static VulkanAllocatorData* s_Data = nullptr; + struct AllocInfo + { + uint64_t AllocatedSize = 0; + AllocationType Type = AllocationType::None; + }; + static std::map s_AllocationMap; + + VulkanAllocator::VulkanAllocator(const std::string& tag) + : m_Tag(tag) + { + } + + VulkanAllocator::~VulkanAllocator() + { + } + +#if 0 + void VulkanAllocator::Allocate(VkMemoryRequirements requirements, VkDeviceMemory* dest, VkMemoryPropertyFlags flags /*= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT*/) + { + SE_CORE_ASSERT(m_Device); + + // TODO: Tracking + SE_CORE_TRACE("VulkanAllocator ({0}): allocating {1}", m_Tag, Utils::BytesToString(requirements.size)); + + { + static uint64_t totalAllocatedBytes = 0; + totalAllocatedBytes += requirements.size; + SE_CORE_TRACE("VulkanAllocator ({0}): total allocated since start is {1}", m_Tag, Utils::BytesToString(totalAllocatedBytes)); + } + + VkMemoryAllocateInfo memAlloc = {}; + memAlloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + memAlloc.allocationSize = requirements.size; + memAlloc.memoryTypeIndex = m_Device->GetPhysicalDevice()->GetMemoryTypeIndex(requirements.memoryTypeBits, flags); + VK_CHECK_RESULT(vkAllocateMemory(m_Device->GetVulkanDevice(), &memAlloc, nullptr, dest)); + } +#endif + + VmaAllocation VulkanAllocator::AllocateBuffer(VkBufferCreateInfo bufferCreateInfo, VmaMemoryUsage usage, VkBuffer& outBuffer) + { + SE_CORE_VERIFY(bufferCreateInfo.size > 0); + + VmaAllocationCreateInfo allocCreateInfo = {}; + allocCreateInfo.usage = usage; + + VmaAllocation allocation; + vmaCreateBuffer(s_Data->Allocator, &bufferCreateInfo, &allocCreateInfo, &outBuffer, &allocation, nullptr); + if (allocation == nullptr) + { + SE_CORE_ERROR_TAG("Renderer", "Failed to allocate GPU buffer!"); + SE_CORE_ERROR_TAG("Renderer", " Requested size: {}", Utils::BytesToString(bufferCreateInfo.size)); + auto stats = GetStats(); + SE_CORE_ERROR_TAG("Renderer", " GPU mem usage: {}/{}", Utils::BytesToString(stats.Used), Utils::BytesToString(stats.TotalAvailable)); + } + + // TODO: Tracking + VmaAllocationInfo allocInfo{}; + vmaGetAllocationInfo(s_Data->Allocator, allocation, &allocInfo); + SE_ALLOCATOR_LOG("VulkanAllocator ({0}): allocating buffer; size = {1}", m_Tag, Utils::BytesToString(allocInfo.size)); + + { + s_Data->TotalAllocatedBytes += allocInfo.size; + SE_ALLOCATOR_LOG("VulkanAllocator ({0}): total allocated since start is {1}", m_Tag, Utils::BytesToString(s_Data->TotalAllocatedBytes)); + } + +#if SE_GPU_TRACK_MEMORY_ALLOCATION + auto& allocTrack = s_AllocationMap[allocation]; + allocTrack.AllocatedSize = allocInfo.size; + allocTrack.Type = AllocationType::Buffer; + s_Data->MemoryUsage += allocInfo.size; +#endif + + return allocation; + } + + VmaAllocation VulkanAllocator::AllocateImage(VkImageCreateInfo imageCreateInfo, VmaMemoryUsage usage, VkImage& outImage, VkDeviceSize* allocatedSize) + { + VmaAllocationCreateInfo allocCreateInfo = {}; + allocCreateInfo.usage = usage; + + VmaAllocation allocation; + vmaCreateImage(s_Data->Allocator, &imageCreateInfo, &allocCreateInfo, &outImage, &allocation, nullptr); + if (allocation == nullptr) + { + SE_CORE_ERROR_TAG("Renderer", "Failed to allocate GPU image!"); + SE_CORE_ERROR_TAG("Renderer", " Requested size: {}x{}x{}", imageCreateInfo.extent.width, imageCreateInfo.extent.height, imageCreateInfo.extent.depth); + SE_CORE_ERROR_TAG("Renderer", " Mips: {}", imageCreateInfo.mipLevels); + SE_CORE_ERROR_TAG("Renderer", " Layers: {}", imageCreateInfo.arrayLayers); + auto stats = GetStats(); + SE_CORE_ERROR_TAG("Renderer", " GPU mem usage: {}/{}", Utils::BytesToString(stats.Used), Utils::BytesToString(stats.TotalAvailable)); + } + + // TODO: Tracking + VmaAllocationInfo allocInfo; + vmaGetAllocationInfo(s_Data->Allocator, allocation, &allocInfo); + if (allocatedSize) + *allocatedSize = allocInfo.size; + SE_ALLOCATOR_LOG("VulkanAllocator ({0}): allocating image; size = {1}", m_Tag, Utils::BytesToString(allocInfo.size)); + + { + s_Data->TotalAllocatedBytes += allocInfo.size; + SE_ALLOCATOR_LOG("VulkanAllocator ({0}): total allocated since start is {1}", m_Tag, Utils::BytesToString(s_Data->TotalAllocatedBytes)); + } + +#if SE_GPU_TRACK_MEMORY_ALLOCATION + auto& allocTrack = s_AllocationMap[allocation]; + allocTrack.AllocatedSize = allocInfo.size; + allocTrack.Type = AllocationType::Image; + s_Data->MemoryUsage += allocInfo.size; +#endif + + return allocation; + } + + void VulkanAllocator::Free(VmaAllocation allocation) + { + vmaFreeMemory(s_Data->Allocator, allocation); + +#if SE_GPU_TRACK_MEMORY_ALLOCATION + auto it = s_AllocationMap.find(allocation); + if (it != s_AllocationMap.end()) + { + s_Data->MemoryUsage -= it->second.AllocatedSize; + s_AllocationMap.erase(it); + } + else + { + SE_CORE_ERROR("Could not find GPU memory allocation: {}", (void*)allocation); + } +#endif + } + + void VulkanAllocator::DestroyImage(VkImage image, VmaAllocation allocation) + { + SE_CORE_ASSERT(image); + SE_CORE_ASSERT(allocation); + vmaDestroyImage(s_Data->Allocator, image, allocation); + +#if SE_GPU_TRACK_MEMORY_ALLOCATION + auto it = s_AllocationMap.find(allocation); + if (it != s_AllocationMap.end()) + { + s_Data->MemoryUsage -= it->second.AllocatedSize; + s_AllocationMap.erase(it); + } + else + { + SE_CORE_ERROR("Could not find GPU memory allocation: {}", (void*)allocation); + } +#endif + } + + void VulkanAllocator::DestroyBuffer(VkBuffer buffer, VmaAllocation allocation) + { + SE_CORE_ASSERT(buffer); + SE_CORE_ASSERT(allocation); + vmaDestroyBuffer(s_Data->Allocator, buffer, allocation); + +#if SE_GPU_TRACK_MEMORY_ALLOCATION + auto it = s_AllocationMap.find(allocation); + if (it != s_AllocationMap.end()) + { + s_Data->MemoryUsage -= it->second.AllocatedSize; + s_AllocationMap.erase(it); + } + else + { + SE_CORE_ERROR("Could not find GPU memory allocation: {}", (void*)allocation); + } +#endif + } + + void VulkanAllocator::UnmapMemory(VmaAllocation allocation) + { + vmaUnmapMemory(s_Data->Allocator, allocation); + } + + void VulkanAllocator::DumpStats() + { + const auto& memoryProps = VulkanContext::GetCurrentDevice()->GetPhysicalDevice()->GetMemoryProperties(); + std::vector budgets(memoryProps.memoryHeapCount); + vmaGetBudget(s_Data->Allocator, budgets.data()); + + SE_CORE_WARN("-----------------------------------"); + for (VmaBudget& b : budgets) + { + SE_CORE_WARN("VmaBudget.allocationBytes = {0}", Utils::BytesToString(b.allocationBytes)); + SE_CORE_WARN("VmaBudget.blockBytes = {0}", Utils::BytesToString(b.blockBytes)); + SE_CORE_WARN("VmaBudget.usage = {0}", Utils::BytesToString(b.usage)); + SE_CORE_WARN("VmaBudget.budget = {0}", Utils::BytesToString(b.budget)); + } + SE_CORE_WARN("-----------------------------------"); + } + + GPUMemoryStats VulkanAllocator::GetStats() + { + const auto& memoryProps = VulkanContext::GetCurrentDevice()->GetPhysicalDevice()->GetMemoryProperties(); + std::vector budgets(memoryProps.memoryHeapCount); + vmaGetBudget(s_Data->Allocator, budgets.data()); + + uint64_t budget = 0; + for (VmaBudget& b : budgets) + budget += b.budget; + + GPUMemoryStats result; + for (const auto& [k, v] : s_AllocationMap) + { + if (v.Type == AllocationType::Buffer) + { + result.BufferAllocationCount++; + result.BufferAllocationSize += v.AllocatedSize; + } + else if (v.Type == AllocationType::Image) + { + result.ImageAllocationCount++; + result.ImageAllocationSize += v.AllocatedSize; + } + } + + result.AllocationCount = s_AllocationMap.size(); + result.Used = s_Data->MemoryUsage; + result.TotalAvailable = budget; + return result; +#if 0 + VmaStats stats; + vmaCalculateStats(s_Data->Allocator, &stats); + + uint64_t usedMemory = stats.total.usedBytes; + uint64_t freeMemory = stats.total.unusedBytes; + + return { usedMemory, freeMemory }; +#endif + } + + void VulkanAllocator::Init(Ref device) + { + s_Data = snew VulkanAllocatorData(); + + // Initialize VulkanMemoryAllocator + VmaAllocatorCreateInfo allocatorInfo = {}; + allocatorInfo.vulkanApiVersion = VK_API_VERSION_1_2; + allocatorInfo.physicalDevice = device->GetPhysicalDevice()->GetVulkanPhysicalDevice(); + allocatorInfo.device = device->GetVulkanDevice(); + allocatorInfo.instance = VulkanContext::GetInstance(); + + vmaCreateAllocator(&allocatorInfo, &s_Data->Allocator); + } + + void VulkanAllocator::Shutdown() + { + vmaDestroyAllocator(s_Data->Allocator); + + delete s_Data; + s_Data = nullptr; + } + + VmaAllocator& VulkanAllocator::GetVMAAllocator() + { + return s_Data->Allocator; + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAllocator.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAllocator.h new file mode 100644 index 00000000..7e3f143e --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanAllocator.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include "Vulkan.h" +#include "VulkanDevice.h" +#include "VulkanMemoryAllocator/vk_mem_alloc.h" + +#include "StarEngine/Renderer/GPUStats.h" + +namespace StarEngine { + + class VulkanAllocator + { + public: + VulkanAllocator() = default; + VulkanAllocator(const std::string& tag); + ~VulkanAllocator(); + + //void Allocate(VkMemoryRequirements requirements, VkDeviceMemory* dest, VkMemoryPropertyFlags flags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + VmaAllocation AllocateBuffer(VkBufferCreateInfo bufferCreateInfo, VmaMemoryUsage usage, VkBuffer& outBuffer); + VmaAllocation AllocateImage(VkImageCreateInfo imageCreateInfo, VmaMemoryUsage usage, VkImage& outImage, VkDeviceSize* allocatedSize = nullptr); + void Free(VmaAllocation allocation); + void DestroyImage(VkImage image, VmaAllocation allocation); + void DestroyBuffer(VkBuffer buffer, VmaAllocation allocation); + + template + T* MapMemory(VmaAllocation allocation) + { + T* mappedMemory; + vmaMapMemory(VulkanAllocator::GetVMAAllocator(), allocation, (void**)&mappedMemory); + return mappedMemory; + } + + void UnmapMemory(VmaAllocation allocation); + + static void DumpStats(); + static GPUMemoryStats GetStats(); + + static void Init(Ref device); + static void Shutdown(); + + static VmaAllocator& GetVMAAllocator(); + private: + std::string m_Tag; + }; + + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanComputePipeline.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanComputePipeline.cpp new file mode 100644 index 00000000..27d42720 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanComputePipeline.cpp @@ -0,0 +1,281 @@ +#include "sepch.h" +#if TODO +#include "VulkanComputePipeline.h" + +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Platform/Vulkan/VulkanContext.h" +#include "StarEngine/Platform/Vulkan/VulkanDiagnostics.h" +#include "StarEngine/Platform/Vulkan/VulkanStorageBuffer.h" +#include "StarEngine/Renderer/Renderer.h" + +#include + +namespace StarEngine { + + static VkFence s_ComputeFence = nullptr; + + VulkanComputePipeline::VulkanComputePipeline(Ref computeShader) + : m_Shader(computeShader.As()) + { + Ref instance = this; + Renderer::Submit([instance]() mutable + { + instance->RT_CreatePipeline(); + }); + Renderer::RegisterShaderDependency(computeShader, this); + } + + void VulkanComputePipeline::CreatePipeline() + { + Renderer::Submit([instance = Ref(this)]() mutable + { + instance->RT_CreatePipeline(); + }); + } + + void VulkanComputePipeline::BufferMemoryBarrier(Ref renderCommandBuffer, Ref storageBuffer, ResourceAccessFlags fromAccess, ResourceAccessFlags toAccess) + { + BufferMemoryBarrier(renderCommandBuffer, storageBuffer, PipelineStage::ComputeShader, fromAccess, PipelineStage::ComputeShader, toAccess); + } + + void VulkanComputePipeline::BufferMemoryBarrier(Ref renderCommandBuffer, Ref storageBuffer, PipelineStage fromStage, ResourceAccessFlags fromAccess, PipelineStage toStage, ResourceAccessFlags toAccess) + { + Renderer::Submit([vulkanRenderCommandBuffer = renderCommandBuffer.As(), vulkanStorageBuffer = storageBuffer.As(), fromStage, fromAccess, toStage, toAccess]() mutable + { + VkBufferMemoryBarrier bufferMemoryBarrier = {}; + bufferMemoryBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + bufferMemoryBarrier.buffer = vulkanStorageBuffer->GetVulkanBuffer(); + bufferMemoryBarrier.offset = 0; + bufferMemoryBarrier.size = VK_WHOLE_SIZE; + bufferMemoryBarrier.srcAccessMask = (VkAccessFlags)fromAccess; + bufferMemoryBarrier.dstAccessMask = (VkAccessFlags)toAccess; + vkCmdPipelineBarrier( + vulkanRenderCommandBuffer->GetActiveCommandBuffer(), + (VkPipelineStageFlagBits)fromStage, + (VkPipelineStageFlagBits)toStage, + 0, + 0, nullptr, + 1, &bufferMemoryBarrier, + 0, nullptr); + }); + } + + void VulkanComputePipeline::ImageMemoryBarrier(Ref renderCommandBuffer, Ref image, ResourceAccessFlags fromAccess, ResourceAccessFlags toAccess) + { + ImageMemoryBarrier(renderCommandBuffer, image, PipelineStage::ComputeShader, fromAccess, PipelineStage::ComputeShader, toAccess); + } + + void VulkanComputePipeline::ImageMemoryBarrier(Ref renderCommandBuffer, Ref image, PipelineStage fromStage, ResourceAccessFlags fromAccess, PipelineStage toStage, ResourceAccessFlags toAccess) + { + Renderer::Submit([vulkanRenderCommandBuffer = renderCommandBuffer.As(), vulkanImage = image.As(), fromStage, fromAccess, toStage, toAccess]() mutable + { + VkImageLayout imageLayout = vulkanImage->GetDescriptorInfoVulkan().imageLayout; + + VkImageMemoryBarrier imageMemoryBarrier = {}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.oldLayout = imageLayout; + imageMemoryBarrier.newLayout = imageLayout; + imageMemoryBarrier.image = vulkanImage->GetImageInfo().Image; + // TODO(Yan): get layer count from image; also take SubresourceRange as parameter + imageMemoryBarrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, vulkanImage->GetSpecification().Mips, 0, 1 }; + imageMemoryBarrier.srcAccessMask = (VkAccessFlags)fromAccess; + imageMemoryBarrier.dstAccessMask = (VkAccessFlags)toAccess; + vkCmdPipelineBarrier( + vulkanRenderCommandBuffer->GetActiveCommandBuffer(), + (VkPipelineStageFlagBits)fromStage, + (VkPipelineStageFlagBits)toStage, + 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + }); + } + + void VulkanComputePipeline::RT_CreatePipeline() + { + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + // TODO: Abstract into some sort of compute pipeline + + auto descriptorSetLayouts = m_Shader->GetAllDescriptorSetLayouts(); + + VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo {}; + pipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutCreateInfo.setLayoutCount = (uint32_t)descriptorSetLayouts.size(); + pipelineLayoutCreateInfo.pSetLayouts = descriptorSetLayouts.data(); + + const auto& pushConstantRanges = m_Shader->GetPushConstantRanges(); + std::vector vulkanPushConstantRanges(pushConstantRanges.size()); + if (pushConstantRanges.size()) + { + // TODO: should come from shader + for (uint32_t i = 0; i < pushConstantRanges.size(); i++) + { + const auto& pushConstantRange = pushConstantRanges[i]; + auto& vulkanPushConstantRange = vulkanPushConstantRanges[i]; + + vulkanPushConstantRange.stageFlags = (int)pushConstantRange.ShaderStage; + vulkanPushConstantRange.offset = pushConstantRange.Offset; + vulkanPushConstantRange.size = pushConstantRange.Size; + } + + pipelineLayoutCreateInfo.pushConstantRangeCount = (uint32_t)vulkanPushConstantRanges.size(); + pipelineLayoutCreateInfo.pPushConstantRanges = vulkanPushConstantRanges.data(); + } + + VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &m_ComputePipelineLayout)); + + VkComputePipelineCreateInfo computePipelineCreateInfo {}; + computePipelineCreateInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + computePipelineCreateInfo.layout = m_ComputePipelineLayout; + computePipelineCreateInfo.flags = 0; + const auto& shaderStages = m_Shader->GetPipelineShaderStageCreateInfos(); + computePipelineCreateInfo.stage = shaderStages[0]; + + VkPipelineCacheCreateInfo pipelineCacheCreateInfo = {}; + pipelineCacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + + VK_CHECK_RESULT(vkCreatePipelineCache(device, &pipelineCacheCreateInfo, nullptr, &m_PipelineCache)); + VK_CHECK_RESULT(vkCreateComputePipelines(device, m_PipelineCache, 1, &computePipelineCreateInfo, nullptr, &m_ComputePipeline)); + + VKUtils::SetDebugUtilsObjectName(device, VK_OBJECT_TYPE_PIPELINE, m_Shader->GetName(), m_ComputePipeline); + } + + void VulkanComputePipeline::Execute(VkDescriptorSet* descriptorSets, uint32_t descriptorSetCount, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) + { + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + VkQueue computeQueue = VulkanContext::GetCurrentDevice()->GetComputeQueue(); + //vkQueueWaitIdle(computeQueue); // TODO: don't + + VkCommandBuffer computeCommandBuffer = VulkanContext::GetCurrentDevice()->GetCommandBuffer(true, true); + + Utils::SetVulkanCheckpoint(computeCommandBuffer, "VulkanComputePipeline::Execute"); + + vkCmdBindPipeline(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_ComputePipeline); + for (uint32_t i = 0; i < descriptorSetCount; i++) + { + vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_ComputePipelineLayout, 0, 1, &descriptorSets[i], 0, 0); + vkCmdDispatch(computeCommandBuffer, groupCountX, groupCountY, groupCountZ); + } + + vkEndCommandBuffer(computeCommandBuffer); + if (!s_ComputeFence) + { + + VkFenceCreateInfo fenceCreateInfo {}; + fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + VK_CHECK_RESULT(vkCreateFence(device, &fenceCreateInfo, nullptr, &s_ComputeFence)); + + VKUtils::SetDebugUtilsObjectName(device, VK_OBJECT_TYPE_FENCE, std::format("Compute pipeline fence"), s_ComputeFence); + } + + // Make sure previous compute shader in pipeline has completed (TODO: this shouldn't be needed for all cases) + vkWaitForFences(device, 1, &s_ComputeFence, VK_TRUE, UINT64_MAX); + vkResetFences(device, 1, &s_ComputeFence); + + VkSubmitInfo computeSubmitInfo {}; + computeSubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + computeSubmitInfo.commandBufferCount = 1; + computeSubmitInfo.pCommandBuffers = &computeCommandBuffer; + VK_CHECK_RESULT(vkQueueSubmit(computeQueue, 1, &computeSubmitInfo, s_ComputeFence)); + + // Wait for execution of compute shader to complete + // Currently this is here for "safety" + { + SE_SCOPE_TIMER("Compute shader execution"); + vkWaitForFences(device, 1, &s_ComputeFence, VK_TRUE, UINT64_MAX); + } + } + + void VulkanComputePipeline::Begin(Ref renderCommandBuffer) + { + SE_CORE_ASSERT(!m_ActiveComputeCommandBuffer); + + if (renderCommandBuffer) + { + uint32_t frameIndex = Renderer::GetCurrentFrameIndex(); + m_ActiveComputeCommandBuffer = renderCommandBuffer.As()->GetCommandBuffer(frameIndex); + m_UsingGraphicsQueue = true; + } + else + { + m_ActiveComputeCommandBuffer = VulkanContext::GetCurrentDevice()->GetCommandBuffer(true, true); + m_UsingGraphicsQueue = false; + } + vkCmdBindPipeline(m_ActiveComputeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_ComputePipeline); + } + + void VulkanComputePipeline::RT_Begin(Ref renderCommandBuffer) + { + SE_CORE_ASSERT(!m_ActiveComputeCommandBuffer); + + if (renderCommandBuffer) + { + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + m_ActiveComputeCommandBuffer = renderCommandBuffer.As()->GetCommandBuffer(frameIndex); + m_UsingGraphicsQueue = true; + } + else + { + m_ActiveComputeCommandBuffer = VulkanContext::GetCurrentDevice()->GetCommandBuffer(true, true); + m_UsingGraphicsQueue = false; + } + vkCmdBindPipeline(m_ActiveComputeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_ComputePipeline); + } + + + void VulkanComputePipeline::Dispatch(const glm::uvec3& workGroups) const + { + SE_CORE_ASSERT(m_ActiveComputeCommandBuffer); + + vkCmdDispatch(m_ActiveComputeCommandBuffer, workGroups.x, workGroups.y, workGroups.z); + } + + void VulkanComputePipeline::End() + { + SE_CORE_ASSERT(m_ActiveComputeCommandBuffer); + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + if (!m_UsingGraphicsQueue) + { + VkQueue computeQueue = VulkanContext::GetCurrentDevice()->GetComputeQueue(); + + vkEndCommandBuffer(m_ActiveComputeCommandBuffer); + + if (!s_ComputeFence) + { + VkFenceCreateInfo fenceCreateInfo {}; + fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + VK_CHECK_RESULT(vkCreateFence(device, &fenceCreateInfo, nullptr, &s_ComputeFence)); + VKUtils::SetDebugUtilsObjectName(device, VK_OBJECT_TYPE_FENCE, "Compute pipeline fence", s_ComputeFence); + } + vkWaitForFences(device, 1, &s_ComputeFence, VK_TRUE, UINT64_MAX); + vkResetFences(device, 1, &s_ComputeFence); + + VkSubmitInfo computeSubmitInfo {}; + computeSubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + computeSubmitInfo.commandBufferCount = 1; + computeSubmitInfo.pCommandBuffers = &m_ActiveComputeCommandBuffer; + VK_CHECK_RESULT(vkQueueSubmit(computeQueue, 1, &computeSubmitInfo, s_ComputeFence)); + + // Wait for execution of compute shader to complete + // Currently this is here for "safety" + { + SE_SCOPE_TIMER("Compute shader execution"); + vkWaitForFences(device, 1, &s_ComputeFence, VK_TRUE, UINT64_MAX); + } + } + m_ActiveComputeCommandBuffer = nullptr; + } + + void VulkanComputePipeline::SetPushConstants(Buffer constants) const + { + vkCmdPushConstants(m_ActiveComputeCommandBuffer, m_ComputePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, constants.Size, constants.Data); + } + + +} +#endif diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanComputePipeline.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanComputePipeline.h new file mode 100644 index 00000000..b4384cec --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanComputePipeline.h @@ -0,0 +1,54 @@ +#pragma once + +#include "StarEngine/Renderer/PipelineCompute.h" + +#include "VulkanShader.h" +#include "StarEngine/Renderer/Texture.h" +#include "VulkanRenderCommandBuffer.h" + +#include "vulkan/vulkan.h" + +namespace StarEngine { + +#if TODO + class VulkanComputePipeline : public PipelineCompute + { + public: + VulkanComputePipeline(Ref computeShader); + + void Execute(VkDescriptorSet* descriptorSets, uint32_t descriptorSetCount, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ); + + virtual void Begin(Ref renderCommandBuffer = nullptr) override; + virtual void RT_Begin(Ref renderCommandBuffer = nullptr) override; + void Dispatch(const glm::uvec3& workGroups) const; + virtual void End() override; + + virtual Ref GetShader() const override { return m_Shader; } + + VkCommandBuffer GetActiveCommandBuffer() { return m_ActiveComputeCommandBuffer; } + VkPipelineLayout GetLayout() const { return m_ComputePipelineLayout; } + + void SetPushConstants(Buffer constants) const; + void CreatePipeline(); + + virtual void BufferMemoryBarrier(Ref renderCommandBuffer, Ref storageBuffer, ResourceAccessFlags fromAccess, ResourceAccessFlags toAccess) override; + virtual void BufferMemoryBarrier(Ref renderCommandBuffer, Ref storageBuffer, PipelineStage fromStage, ResourceAccessFlags fromAccess, PipelineStage toStage, ResourceAccessFlags toAccess) override; + + virtual void ImageMemoryBarrier(Ref renderCommandBuffer, Ref image, ResourceAccessFlags fromAccess, ResourceAccessFlags toAccess) override; + virtual void ImageMemoryBarrier(Ref renderCommandBuffer, Ref image, PipelineStage fromStage, ResourceAccessFlags fromAccess, PipelineStage toStage, ResourceAccessFlags toAccess) override; + private: + void RT_CreatePipeline(); + private: + Ref m_Shader; + + VkPipelineLayout m_ComputePipelineLayout = nullptr; + VkPipelineCache m_PipelineCache = nullptr; + VkPipeline m_ComputePipeline = nullptr; + + VkCommandBuffer m_ActiveComputeCommandBuffer = nullptr; + + bool m_UsingGraphicsQueue = false; + }; + +#endif +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanContext.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanContext.cpp new file mode 100644 index 00000000..c2e13a74 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanContext.cpp @@ -0,0 +1,272 @@ +#include "sepch.h" +#include "VulkanContext.h" + +#include "Vulkan.h" +#include "StarEngine/Renderer/Image.h" + +#include + +#ifdef SE_PLATFORM_WINDOWS +#include +#endif + +#include + +#ifndef VK_API_VERSION_1_2 +#error Wrong Vulkan SDK! Please run scripts/Setup.bat +#endif + + +namespace StarEngine { + +#if defined(SE_DEBUG) || defined(SE_RELEASE) + static bool s_Validation = true; +#else + static bool s_Validation = false; // Let's leave this on for now... +#endif + +#if 0 + static VKAPI_ATTR VkBool32 VKAPI_CALL VulkanDebugReportCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objectType, uint64_t object, size_t location, int32_t messageCode, const char* pLayerPrefix, const char* pMessage, void* pUserData) + { + (void)flags; (void)object; (void)location; (void)messageCode; (void)pUserData; (void)pLayerPrefix; // Unused arguments + SE_CORE_WARN_TAG("Renderer", "VulkanDebugCallback:\n Object Type: {0}\n Message: {1}", objectType, pMessage); + + const auto& imageRefs = VulkanImage2D::GetImageRefs(); + if (strstr(pMessage, "CoreValidation-DrawState-InvalidImageLayout")) + SE_CORE_ASSERT(false); + + return VK_FALSE; + } +#endif + + constexpr const char* VkDebugUtilsMessageType(const VkDebugUtilsMessageTypeFlagsEXT type) + { + switch (type) + { + case VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT: return "General"; + case VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT: return "Validation"; + case VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT: return "Performance"; + default: return "Unknown"; + } + } + + constexpr const char* VkDebugUtilsMessageSeverity(const VkDebugUtilsMessageSeverityFlagBitsEXT severity) + { + switch (severity) + { + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: return "error"; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: return "warning"; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: return "info"; + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: return "verbose"; + default: return "unknown"; + } + } + + static VKAPI_ATTR VkBool32 VKAPI_CALL VulkanDebugUtilsMessengerCallback(const VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, const VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) + { + (void)pUserData; //Unused argument + + const bool performanceWarnings = false; + if (!performanceWarnings) + { + if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) + return VK_FALSE; + } + + std::string labels, objects; + if (pCallbackData->cmdBufLabelCount) + { + labels = std::format("\tLabels({}): \n", pCallbackData->cmdBufLabelCount); + for (uint32_t i = 0; i < pCallbackData->cmdBufLabelCount; ++i) + { + const auto& label = pCallbackData->pCmdBufLabels[i]; + const std::string colorStr = std::format("[ {}, {}, {}, {} ]", label.color[0], label.color[1], label.color[2], label.color[3]); + labels.append(std::format("\t\t- Command Buffer Label[{0}]: name: {1}, color: {2}\n", i, label.pLabelName ? label.pLabelName : "NULL", colorStr)); + } + } + + if (pCallbackData->objectCount) + { + objects = std::format("\tObjects({}): \n", pCallbackData->objectCount); + for (uint32_t i = 0; i < pCallbackData->objectCount; ++i) + { + const auto& object = pCallbackData->pObjects[i]; + objects.append(std::format("\t\t- Object[{0}] name: {1}, type: {2}, handle: {3:#x}\n", i, object.pObjectName ? object.pObjectName : "NULL", Utils::VkObjectTypeToString(object.objectType), object.objectHandle)); + } + } + + SE_CORE_WARN("{0} {1} message: \n\t{2}\n {3} {4}", VkDebugUtilsMessageType(messageType), VkDebugUtilsMessageSeverity(messageSeverity), pCallbackData->pMessage, labels, objects); + [[maybe_unused]] const auto& imageRefs = Image2D::GetImageRefs(); + + return VK_FALSE; + } + + static bool CheckDriverAPIVersionSupport(uint32_t minimumSupportedVersion) + { + uint32_t instanceVersion; + vkEnumerateInstanceVersion(&instanceVersion); + + if (instanceVersion < minimumSupportedVersion) + { + SE_CORE_FATAL("Incompatible Vulkan driver version!"); + SE_CORE_FATAL(" You have {}.{}.{}", VK_API_VERSION_MAJOR(instanceVersion), VK_API_VERSION_MINOR(instanceVersion), VK_API_VERSION_PATCH(instanceVersion)); + SE_CORE_FATAL(" You need at least {}.{}.{}", VK_API_VERSION_MAJOR(minimumSupportedVersion), VK_API_VERSION_MINOR(minimumSupportedVersion), VK_API_VERSION_PATCH(minimumSupportedVersion)); + + return false; + } + + return true; + } + + VulkanContext::VulkanContext() + { + } + + VulkanContext::~VulkanContext() + { + // Its too late to destroy the device here, because Destroy() asks for the context (which we're in the middle of destructing) + // Device is destroyed in GLFWWindow::Shutdown() + //m_Device->Destroy(); + + vkDestroyInstance(s_VulkanInstance, nullptr); + s_VulkanInstance = nullptr; + } + + void VulkanContext::Init() + { + SE_CORE_INFO_TAG("Renderer", "VulkanContext::Create"); + + SE_CORE_ASSERT(glfwVulkanSupported(), "GLFW must support Vulkan!"); + + if (!CheckDriverAPIVersionSupport(VK_API_VERSION_1_2)) + { +#ifdef SE_PLATFORM_WINDOWS + MessageBox(nullptr, L"Incompatible Vulkan driver version.\nUpdate your GPU drivers!", L"StarEngine Error", MB_OK | MB_ICONERROR); +#else + SE_CORE_ERROR("Incompatible Vulkan driver version.\nUpdate your GPU drivers!"); +#endif + SE_CORE_VERIFY(false); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Application Info + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VkApplicationInfo appInfo = {}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "StarEngine"; + appInfo.pEngineName = "StarEngine"; + appInfo.apiVersion = VK_API_VERSION_1_2; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Extensions and Validation + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// TODO(Emily): GLFW can handle this for us +#ifdef SE_PLATFORM_WINDOWS +#define VK_KHR_WIN32_SURFACE_EXTENSION_NAME "VK_KHR_win32_surface" +#elif defined(SE_PLATFORM_LINUX) +#define VK_KHR_WIN32_SURFACE_EXTENSION_NAME "VK_KHR_xcb_surface" +#endif + std::vector instanceExtensions = { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WIN32_SURFACE_EXTENSION_NAME }; + instanceExtensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); // Very little performance hit, can be used in Release. + if (s_Validation) + { + instanceExtensions.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME); + instanceExtensions.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + } + + VkValidationFeatureEnableEXT enables[] = { VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT }; + VkValidationFeaturesEXT features = {}; + features.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT; + features.enabledValidationFeatureCount = 1; + features.pEnabledValidationFeatures = enables; + + VkInstanceCreateInfo instanceCreateInfo = {}; + instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + instanceCreateInfo.pNext = nullptr; // &features; + instanceCreateInfo.pApplicationInfo = &appInfo; + instanceCreateInfo.enabledExtensionCount = (uint32_t)instanceExtensions.size(); + instanceCreateInfo.ppEnabledExtensionNames = instanceExtensions.data(); + + // TODO: Extract all validation into separate class + if (s_Validation) + { + const char* validationLayerName = "VK_LAYER_KHRONOS_validation"; + // Check if this layer is available at instance level + uint32_t instanceLayerCount; + vkEnumerateInstanceLayerProperties(&instanceLayerCount, nullptr); + std::vector instanceLayerProperties(instanceLayerCount); + vkEnumerateInstanceLayerProperties(&instanceLayerCount, instanceLayerProperties.data()); + bool validationLayerPresent = false; + SE_CORE_INFO_TAG("Renderer", "Vulkan Instance Layers:"); + for (const VkLayerProperties& layer : instanceLayerProperties) + { + SE_CORE_INFO_TAG("Renderer", " {0}", layer.layerName); + if (strcmp(layer.layerName, validationLayerName) == 0) + { + validationLayerPresent = true; + break; + } + } + if (validationLayerPresent) + { + instanceCreateInfo.ppEnabledLayerNames = &validationLayerName; + instanceCreateInfo.enabledLayerCount = 1; + } + else + { + SE_CORE_ERROR_TAG("Renderer", "Validation layer VK_LAYER_KHRONOS_validation not present, validation is disabled"); + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Instance and Surface Creation + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + VK_CHECK_RESULT(vkCreateInstance(&instanceCreateInfo, nullptr, &s_VulkanInstance)); + Utils::VulkanLoadDebugUtilsExtensions(s_VulkanInstance); + + if (s_Validation) + { +#if 0 + auto vkCreateDebugReportCallbackEXT = (PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr(s_VulkanInstance, "vkCreateDebugReportCallbackEXT"); + SE_CORE_ASSERT(vkCreateDebugReportCallbackEXT != NULL, ""); + VkDebugReportCallbackCreateInfoEXT debug_report_ci = {}; + debug_report_ci.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT; + debug_report_ci.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT; + debug_report_ci.pfnCallback = VulkanDebugReportCallback; + debug_report_ci.pUserData = VK_NULL_HANDLE; + VK_CHECK_RESULT(vkCreateDebugReportCallbackEXT(s_VulkanInstance, &debug_report_ci, nullptr, &m_DebugReportCallback)); +#endif + + auto vkCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(s_VulkanInstance, "vkCreateDebugUtilsMessengerEXT"); + SE_CORE_ASSERT(vkCreateDebugUtilsMessengerEXT != NULL, ""); + VkDebugUtilsMessengerCreateInfoEXT debugUtilsCreateInfo{}; + debugUtilsCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + debugUtilsCreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + debugUtilsCreateInfo.pfnUserCallback = VulkanDebugUtilsMessengerCallback; + debugUtilsCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT /* | VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT*/; + + VK_CHECK_RESULT(vkCreateDebugUtilsMessengerEXT(s_VulkanInstance, &debugUtilsCreateInfo, nullptr, &m_DebugUtilsMessenger)); + } + + m_PhysicalDevice = VulkanPhysicalDevice::Select(); + + VkPhysicalDeviceFeatures enabledFeatures; + memset(&enabledFeatures, 0, sizeof(VkPhysicalDeviceFeatures)); + enabledFeatures.samplerAnisotropy = true; + enabledFeatures.wideLines = true; + enabledFeatures.fillModeNonSolid = true; + enabledFeatures.independentBlend = true; + enabledFeatures.pipelineStatisticsQuery = true; + enabledFeatures.shaderStorageImageReadWithoutFormat = true; + m_Device = Ref::Create(m_PhysicalDevice, enabledFeatures); + + //VulkanAllocator::Init(m_Device); + + // Pipeline Cache + VkPipelineCacheCreateInfo pipelineCacheCreateInfo = {}; + pipelineCacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + VK_CHECK_RESULT(vkCreatePipelineCache(m_Device->GetVulkanDevice(), &pipelineCacheCreateInfo, nullptr, &m_PipelineCache)); + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanContext.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanContext.h new file mode 100644 index 00000000..d65eb764 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanContext.h @@ -0,0 +1,40 @@ +#pragma once + +#include "StarEngine/Renderer/Renderer.h" + +#include "VulkanDevice.h" + +struct GLFWwindow; + +namespace StarEngine { + + class VulkanContext : public RendererContext + { + public: + VulkanContext(); + virtual ~VulkanContext(); + + virtual void Init() override; + + Ref GetDevice() { return m_Device; } + + static VkInstance GetInstance() { return s_VulkanInstance; } + + static Ref Get() { return Ref(Renderer::GetContext()); } + static Ref GetCurrentDevice() { return Get()->GetDevice(); } + private: + // Devices + Ref m_PhysicalDevice; + Ref m_Device; + + // Vulkan instance + inline static VkInstance s_VulkanInstance; +#if 0 + VkDebugReportCallbackEXT m_DebugReportCallback = VK_NULL_HANDLE; +#endif + VkDebugUtilsMessengerEXT m_DebugUtilsMessenger = VK_NULL_HANDLE; + VkPipelineCache m_PipelineCache = nullptr; + + // VulkanSwapChain m_SwapChain; + }; +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDevice.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDevice.cpp new file mode 100644 index 00000000..33f04d2c --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDevice.cpp @@ -0,0 +1,492 @@ +#include "sepch.h" +#include "VulkanDevice.h" + +#include "VulkanContext.h" +#include "VulkanMemoryAllocator/vk_mem_alloc.h" + +#define SE_HAS_AFTERMATH !SE_DIST + +#if SE_HAS_AFTERMATH +#include "Debug/NsightAftermathGpuCrashTracker.h" +#endif + +namespace StarEngine { + + //////////////////////////////////////////////////////////////////////////////////// + // Vulkan Physical Device + //////////////////////////////////////////////////////////////////////////////////// + + VulkanPhysicalDevice::VulkanPhysicalDevice() + { + auto vkInstance = VulkanContext::GetInstance(); + + uint32_t gpuCount = 0; + // Get number of available physical devices + vkEnumeratePhysicalDevices(vkInstance, &gpuCount, nullptr); + SE_CORE_ASSERT(gpuCount > 0, ""); + // Enumerate devices + std::vector physicalDevices(gpuCount); + VK_CHECK_RESULT(vkEnumeratePhysicalDevices(vkInstance, &gpuCount, physicalDevices.data())); + + VkPhysicalDevice selectedPhysicalDevice = nullptr; + for (VkPhysicalDevice physicalDevice : physicalDevices) + { + vkGetPhysicalDeviceProperties(physicalDevice, &m_Properties); + if (m_Properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) + { + selectedPhysicalDevice = physicalDevice; + break; + } + } + + if (!selectedPhysicalDevice) + { + SE_CORE_INFO_TAG("Renderer", "Could not find discrete GPU."); + selectedPhysicalDevice = physicalDevices.back(); + } + + SE_CORE_ASSERT(selectedPhysicalDevice, "Could not find any physical devices!"); + m_PhysicalDevice = selectedPhysicalDevice; + + vkGetPhysicalDeviceFeatures(m_PhysicalDevice, &m_Features); + vkGetPhysicalDeviceMemoryProperties(m_PhysicalDevice, &m_MemoryProperties); + + uint32_t queueFamilyCount; + vkGetPhysicalDeviceQueueFamilyProperties(m_PhysicalDevice, &queueFamilyCount, nullptr); + SE_CORE_ASSERT(queueFamilyCount > 0, ""); + m_QueueFamilyProperties.resize(queueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(m_PhysicalDevice, &queueFamilyCount, m_QueueFamilyProperties.data()); + + uint32_t extCount = 0; + vkEnumerateDeviceExtensionProperties(m_PhysicalDevice, nullptr, &extCount, nullptr); + if (extCount > 0) + { + std::vector extensions(extCount); + if (vkEnumerateDeviceExtensionProperties(m_PhysicalDevice, nullptr, &extCount, &extensions.front()) == VK_SUCCESS) + { + SE_CORE_INFO_TAG("Renderer", "Selected physical device has {0} extensions", extensions.size()); + for (const auto& ext : extensions) + { + m_SupportedExtensions.emplace(ext.extensionName); + SE_CORE_INFO_TAG("Renderer", " {0}", ext.extensionName); + } + } + } + + // Queue families + // Desired queues need to be requested upon logical device creation + // Due to differing queue family configurations of Vulkan implementations this can be a bit tricky, especially if the application + // requests different queue types + + // Get queue family indices for the requested queue family types + // Note that the indices may overlap depending on the implementation + + static const float defaultQueuePriority(0.0f); + + int requestedQueueTypes = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT; + m_QueueFamilyIndices = GetQueueFamilyIndices(requestedQueueTypes); + + // Graphics queue + if (requestedQueueTypes & VK_QUEUE_GRAPHICS_BIT) + { + VkDeviceQueueCreateInfo queueInfo{}; + queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueInfo.queueFamilyIndex = m_QueueFamilyIndices.Graphics; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &defaultQueuePriority; + m_QueueCreateInfos.push_back(queueInfo); + } + + // Dedicated compute queue + if (requestedQueueTypes & VK_QUEUE_COMPUTE_BIT) + { + if (m_QueueFamilyIndices.Compute != m_QueueFamilyIndices.Graphics) + { + // If compute family index differs, we need an additional queue create info for the compute queue + VkDeviceQueueCreateInfo queueInfo{}; + queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueInfo.queueFamilyIndex = m_QueueFamilyIndices.Compute; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &defaultQueuePriority; + m_QueueCreateInfos.push_back(queueInfo); + } + } + + // Dedicated transfer queue + if (requestedQueueTypes & VK_QUEUE_TRANSFER_BIT) + { + if ((m_QueueFamilyIndices.Transfer != m_QueueFamilyIndices.Graphics) && (m_QueueFamilyIndices.Transfer != m_QueueFamilyIndices.Compute)) + { + // If compute family index differs, we need an additional queue create info for the compute queue + VkDeviceQueueCreateInfo queueInfo{}; + queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueInfo.queueFamilyIndex = m_QueueFamilyIndices.Transfer; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &defaultQueuePriority; + m_QueueCreateInfos.push_back(queueInfo); + } + } + + m_DepthFormat = FindDepthFormat(); + SE_CORE_ASSERT(m_DepthFormat); + } + + VulkanPhysicalDevice::~VulkanPhysicalDevice() + { + } + + VkFormat VulkanPhysicalDevice::FindDepthFormat() const + { + // Since all depth formats may be optional, we need to find a suitable depth format to use + // Start with the highest precision packed format + std::vector depthFormats = { + VK_FORMAT_D32_SFLOAT_S8_UINT, + VK_FORMAT_D32_SFLOAT, + VK_FORMAT_D24_UNORM_S8_UINT, + VK_FORMAT_D16_UNORM_S8_UINT, + VK_FORMAT_D16_UNORM + }; + + // TODO: Move to VulkanPhysicalDevice + for (auto& format : depthFormats) + { + VkFormatProperties formatProps; + vkGetPhysicalDeviceFormatProperties(m_PhysicalDevice, format, &formatProps); + // Format must support depth stencil attachment for optimal tiling + if (formatProps.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) + return format; + } + return VK_FORMAT_UNDEFINED; + } + + bool VulkanPhysicalDevice::IsExtensionSupported(const std::string& extensionName) const + { + return m_SupportedExtensions.find(extensionName) != m_SupportedExtensions.end(); + } + + VulkanPhysicalDevice::QueueFamilyIndices VulkanPhysicalDevice::GetQueueFamilyIndices(int flags) + { + QueueFamilyIndices indices; + + // Dedicated queue for compute + // Try to find a queue family index that supports compute but not graphics + if (flags & VK_QUEUE_COMPUTE_BIT) + { + for (uint32_t i = 0; i < m_QueueFamilyProperties.size(); i++) + { + auto& queueFamilyProperties = m_QueueFamilyProperties[i]; + if ((queueFamilyProperties.queueFlags & VK_QUEUE_COMPUTE_BIT) && ((queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0)) + { + indices.Compute = i; + break; + } + } + } + + // Dedicated queue for transfer + // Try to find a queue family index that supports transfer but not graphics and compute + if (flags & VK_QUEUE_TRANSFER_BIT) + { + for (uint32_t i = 0; i < m_QueueFamilyProperties.size(); i++) + { + auto& queueFamilyProperties = m_QueueFamilyProperties[i]; + if ((queueFamilyProperties.queueFlags & VK_QUEUE_TRANSFER_BIT) && ((queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) && ((queueFamilyProperties.queueFlags & VK_QUEUE_COMPUTE_BIT) == 0)) + { + indices.Transfer = i; + break; + } + } + } + + // For other queue types or if no separate compute queue is present, return the first one to support the requested flags + for (uint32_t i = 0; i < m_QueueFamilyProperties.size(); i++) + { + if ((flags & VK_QUEUE_TRANSFER_BIT) && indices.Transfer == -1) + { + if (m_QueueFamilyProperties[i].queueFlags & VK_QUEUE_TRANSFER_BIT) + indices.Transfer = i; + } + + if ((flags & VK_QUEUE_COMPUTE_BIT) && indices.Compute == -1) + { + if (m_QueueFamilyProperties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) + indices.Compute = i; + } + + if (flags & VK_QUEUE_GRAPHICS_BIT) + { + if (m_QueueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) + indices.Graphics = i; + } + } + + return indices; + } + + uint32_t VulkanPhysicalDevice::GetMemoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags properties) const + { + // Iterate over all memory types available for the device used in this example + for (uint32_t i = 0; i < m_MemoryProperties.memoryTypeCount; i++) + { + if ((typeBits & 1) == 1) + { + if ((m_MemoryProperties.memoryTypes[i].propertyFlags & properties) == properties) + return i; + } + typeBits >>= 1; + } + + SE_CORE_ASSERT(false, "Could not find a suitable memory type!"); + return UINT32_MAX; + } + + Ref VulkanPhysicalDevice::Select() + { + return Ref::Create(); + } + + //////////////////////////////////////////////////////////////////////////////////// + // Vulkan Device + //////////////////////////////////////////////////////////////////////////////////// + + VulkanDevice::VulkanDevice(const Ref& physicalDevice, VkPhysicalDeviceFeatures enabledFeatures) + : m_PhysicalDevice(physicalDevice), m_EnabledFeatures(enabledFeatures) + { + const bool enableAftermath = true; + + // Do we need to enable any other extensions (eg. NV_RAYTRACING?) + std::vector deviceExtensions; + // If the device will be used for presenting to a display via a swapchain we need to request the swapchain extension + SE_CORE_ASSERT(m_PhysicalDevice->IsExtensionSupported(VK_KHR_SWAPCHAIN_EXTENSION_NAME)); + deviceExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + + if (m_PhysicalDevice->IsExtensionSupported(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME)) + deviceExtensions.push_back(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); + if (m_PhysicalDevice->IsExtensionSupported(VK_NV_DEVICE_DIAGNOSTICS_CONFIG_EXTENSION_NAME)) + deviceExtensions.push_back(VK_NV_DEVICE_DIAGNOSTICS_CONFIG_EXTENSION_NAME); + +#if SE_HAS_AFTERMATH + VkDeviceDiagnosticsConfigCreateInfoNV aftermathInfo = {}; + bool canEnableAftermath = enableAftermath && m_PhysicalDevice->IsExtensionSupported(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME) && m_PhysicalDevice->IsExtensionSupported(VK_NV_DEVICE_DIAGNOSTICS_CONFIG_EXTENSION_NAME); + if (canEnableAftermath) + { + // Must be initialized ~before~ device has been created + GpuCrashTracker* gpuCrashTracker = snew GpuCrashTracker(); + gpuCrashTracker->Initialize(); + + VkDeviceDiagnosticsConfigFlagBitsNV aftermathFlags = (VkDeviceDiagnosticsConfigFlagBitsNV)(VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_RESOURCE_TRACKING_BIT_NV | + VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_AUTOMATIC_CHECKPOINTS_BIT_NV | + VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_SHADER_DEBUG_INFO_BIT_NV); + + aftermathInfo.sType = VK_STRUCTURE_TYPE_DEVICE_DIAGNOSTICS_CONFIG_CREATE_INFO_NV; + aftermathInfo.flags = aftermathFlags; + } +#endif + + VkDeviceCreateInfo deviceCreateInfo = {}; + deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; +#if SE_HAS_AFTERMATH + if (canEnableAftermath) + deviceCreateInfo.pNext = &aftermathInfo; +#endif + deviceCreateInfo.queueCreateInfoCount = static_cast(physicalDevice->m_QueueCreateInfos.size());; + deviceCreateInfo.pQueueCreateInfos = physicalDevice->m_QueueCreateInfos.data(); + deviceCreateInfo.pEnabledFeatures = &enabledFeatures; + + // If a pNext(Chain) has been passed, we need to add it to the device creation info + VkPhysicalDeviceFeatures2 physicalDeviceFeatures2{}; + + // Enable the debug marker extension if it is present (likely meaning a debugging tool is present) + if (m_PhysicalDevice->IsExtensionSupported(VK_EXT_DEBUG_MARKER_EXTENSION_NAME)) + { + deviceExtensions.push_back(VK_EXT_DEBUG_MARKER_EXTENSION_NAME); + m_EnableDebugMarkers = true; + } + + if (deviceExtensions.size() > 0) + { + deviceCreateInfo.enabledExtensionCount = (uint32_t)deviceExtensions.size(); + deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data(); + } + + VkResult result = vkCreateDevice(m_PhysicalDevice->GetVulkanPhysicalDevice(), &deviceCreateInfo, nullptr, &m_LogicalDevice); + SE_CORE_ASSERT(result == VK_SUCCESS); + + // Get a graphics queue from the device + vkGetDeviceQueue(m_LogicalDevice, m_PhysicalDevice->m_QueueFamilyIndices.Graphics, 0, &m_GraphicsQueue); + vkGetDeviceQueue(m_LogicalDevice, m_PhysicalDevice->m_QueueFamilyIndices.Compute, 0, &m_ComputeQueue); + } + + VulkanDevice::~VulkanDevice() + { + } + + void VulkanDevice::Destroy() + { + m_CommandPools.clear(); + vkDeviceWaitIdle(m_LogicalDevice); + vkDestroyDevice(m_LogicalDevice, nullptr); + } + + void VulkanDevice::LockQueue(bool compute) + { + if (compute) + m_ComputeQueueMutex.lock(); + else + m_GraphicsQueueMutex.lock(); + } + + void VulkanDevice::UnlockQueue(bool compute) + { + if (compute) + m_ComputeQueueMutex.unlock(); + else + m_GraphicsQueueMutex.unlock(); + } + + VkCommandBuffer VulkanDevice::GetCommandBuffer(bool begin, bool compute) + { + return GetOrCreateThreadLocalCommandPool()->AllocateCommandBuffer(begin, compute); + } + + void VulkanDevice::FlushCommandBuffer(VkCommandBuffer commandBuffer) + { + GetThreadLocalCommandPool()->FlushCommandBuffer(commandBuffer); + } + + void VulkanDevice::FlushCommandBuffer(VkCommandBuffer commandBuffer, VkQueue queue) + { + GetThreadLocalCommandPool()->FlushCommandBuffer(commandBuffer); + } + + VkCommandBuffer VulkanDevice::CreateSecondaryCommandBuffer(const char* debugName) + { + VkCommandBuffer cmdBuffer; + + VkCommandBufferAllocateInfo cmdBufAllocateInfo = {}; + cmdBufAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + cmdBufAllocateInfo.commandPool = GetOrCreateThreadLocalCommandPool()->GetGraphicsCommandPool(); + cmdBufAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY; + cmdBufAllocateInfo.commandBufferCount = 1; + + VK_CHECK_RESULT(vkAllocateCommandBuffers(m_LogicalDevice, &cmdBufAllocateInfo, &cmdBuffer)); + VKUtils::SetDebugUtilsObjectName(m_LogicalDevice, VK_OBJECT_TYPE_COMMAND_BUFFER, debugName, cmdBuffer); + return cmdBuffer; + } + + Ref VulkanDevice::GetThreadLocalCommandPool() + { + auto threadID = std::this_thread::get_id(); + SE_CORE_VERIFY(m_CommandPools.find(threadID) != m_CommandPools.end()); + + return m_CommandPools.at(threadID); + } + + Ref VulkanDevice::GetOrCreateThreadLocalCommandPool() + { + auto threadID = std::this_thread::get_id(); + auto commandPoolIt = m_CommandPools.find(threadID); + if (commandPoolIt != m_CommandPools.end()) + return commandPoolIt->second; + + Ref commandPool = Ref::Create(); + m_CommandPools[threadID] = commandPool; + return commandPool; + } + + VulkanCommandPool::VulkanCommandPool() + { + auto device = VulkanContext::GetCurrentDevice(); + auto vulkanDevice = device->GetVulkanDevice(); + + VkCommandPoolCreateInfo cmdPoolInfo = {}; + cmdPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + cmdPoolInfo.queueFamilyIndex = device->GetPhysicalDevice()->GetQueueFamilyIndices().Graphics; + cmdPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + VK_CHECK_RESULT(vkCreateCommandPool(vulkanDevice, &cmdPoolInfo, nullptr, &m_GraphicsCommandPool)); + + cmdPoolInfo.queueFamilyIndex = device->GetPhysicalDevice()->GetQueueFamilyIndices().Compute; + VK_CHECK_RESULT(vkCreateCommandPool(vulkanDevice, &cmdPoolInfo, nullptr, &m_ComputeCommandPool)); + } + + VulkanCommandPool::~VulkanCommandPool() + { + auto device = VulkanContext::GetCurrentDevice(); + auto vulkanDevice = device->GetVulkanDevice(); + + vkDestroyCommandPool(vulkanDevice, m_GraphicsCommandPool, nullptr); + vkDestroyCommandPool(vulkanDevice, m_ComputeCommandPool, nullptr); + } + + VkCommandBuffer VulkanCommandPool::AllocateCommandBuffer(bool begin, bool compute) + { + auto device = VulkanContext::GetCurrentDevice(); + auto vulkanDevice = device->GetVulkanDevice(); + + VkCommandBuffer cmdBuffer; + + VkCommandBufferAllocateInfo cmdBufAllocateInfo = {}; + cmdBufAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + cmdBufAllocateInfo.commandPool = compute ? m_ComputeCommandPool : m_GraphicsCommandPool; + cmdBufAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + cmdBufAllocateInfo.commandBufferCount = 1; + + VK_CHECK_RESULT(vkAllocateCommandBuffers(vulkanDevice, &cmdBufAllocateInfo, &cmdBuffer)); + + // If requested, also start the new command buffer + if (begin) + { + VkCommandBufferBeginInfo cmdBufferBeginInfo{}; + cmdBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + VK_CHECK_RESULT(vkBeginCommandBuffer(cmdBuffer, &cmdBufferBeginInfo)); + } + + return cmdBuffer; + } + + void VulkanCommandPool::FlushCommandBuffer(VkCommandBuffer commandBuffer) + { + auto device = VulkanContext::GetCurrentDevice(); + FlushCommandBuffer(commandBuffer, device->GetGraphicsQueue()); + } + + void VulkanCommandPool::FlushCommandBuffer(VkCommandBuffer commandBuffer, VkQueue queue) + { + auto device = VulkanContext::GetCurrentDevice(); + SE_CORE_VERIFY(queue == device->GetGraphicsQueue()); + auto vulkanDevice = device->GetVulkanDevice(); + + const uint64_t DEFAULT_FENCE_TIMEOUT = 100000000000; + + SE_CORE_ASSERT(commandBuffer != VK_NULL_HANDLE); + + VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer)); + + VkSubmitInfo submitInfo = {}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + + // Create fence to ensure that the command buffer has finished executing + VkFenceCreateInfo fenceCreateInfo = {}; + fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceCreateInfo.flags = 0; + VkFence fence; + VK_CHECK_RESULT(vkCreateFence(vulkanDevice, &fenceCreateInfo, nullptr, &fence)); + + { + device->LockQueue(); + + // Submit to the queue + VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, fence)); + + device->UnlockQueue(); + } + // Wait for the fence to signal that command buffer has finished executing + VK_CHECK_RESULT(vkWaitForFences(vulkanDevice, 1, &fence, VK_TRUE, DEFAULT_FENCE_TIMEOUT)); + + vkDestroyFence(vulkanDevice, fence, nullptr); + vkFreeCommandBuffers(vulkanDevice, m_GraphicsCommandPool, 1, &commandBuffer); + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDevice.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDevice.h new file mode 100644 index 00000000..e0ca2697 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDevice.h @@ -0,0 +1,113 @@ +#pragma once + +#include "StarEngine/Core/Ref.h" + +#include "Vulkan.h" + +#include + +namespace StarEngine { + + class VulkanPhysicalDevice : public RefCounted + { + public: + struct QueueFamilyIndices + { + int32_t Graphics = -1; + int32_t Compute = -1; + int32_t Transfer = -1; + }; + public: + VulkanPhysicalDevice(); + ~VulkanPhysicalDevice(); + + bool IsExtensionSupported(const std::string& extensionName) const; + uint32_t GetMemoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags properties) const; + + VkPhysicalDevice GetVulkanPhysicalDevice() const { return m_PhysicalDevice; } + const QueueFamilyIndices& GetQueueFamilyIndices() const { return m_QueueFamilyIndices; } + + const VkPhysicalDeviceProperties& GetProperties() const { return m_Properties; } + const VkPhysicalDeviceLimits& GetLimits() const { return m_Properties.limits; } + const VkPhysicalDeviceMemoryProperties& GetMemoryProperties() const { return m_MemoryProperties; } + + VkFormat GetDepthFormat() const { return m_DepthFormat; } + + static Ref Select(); + private: + VkFormat FindDepthFormat() const; + QueueFamilyIndices GetQueueFamilyIndices(int queueFlags); + private: + QueueFamilyIndices m_QueueFamilyIndices; + + VkPhysicalDevice m_PhysicalDevice = nullptr; + VkPhysicalDeviceProperties m_Properties; + VkPhysicalDeviceFeatures m_Features; + VkPhysicalDeviceMemoryProperties m_MemoryProperties; + + VkFormat m_DepthFormat = VK_FORMAT_UNDEFINED; + + std::vector m_QueueFamilyProperties; + std::unordered_set m_SupportedExtensions; + std::vector m_QueueCreateInfos; + + friend class VulkanDevice; + }; + + class VulkanCommandPool : public RefCounted + { + public: + VulkanCommandPool(); + virtual ~VulkanCommandPool(); + + VkCommandBuffer AllocateCommandBuffer(bool begin, bool compute = false); + void FlushCommandBuffer(VkCommandBuffer commandBuffer); + void FlushCommandBuffer(VkCommandBuffer commandBuffer, VkQueue queue); + + VkCommandPool GetGraphicsCommandPool() const { return m_GraphicsCommandPool; } + VkCommandPool GetComputeCommandPool() const { return m_ComputeCommandPool; } + private: + VkCommandPool m_GraphicsCommandPool, m_ComputeCommandPool; + }; + + // Represents a logical device + class VulkanDevice : public RefCounted + { + public: + VulkanDevice(const Ref& physicalDevice, VkPhysicalDeviceFeatures enabledFeatures); + ~VulkanDevice(); + + void Destroy(); + + void LockQueue(bool compute = false); + void UnlockQueue(bool compute = false); + VkQueue GetGraphicsQueue() { return m_GraphicsQueue; } + VkQueue GetComputeQueue() { return m_ComputeQueue; } + + VkCommandBuffer GetCommandBuffer(bool begin, bool compute = false); + void FlushCommandBuffer(VkCommandBuffer commandBuffer); + void FlushCommandBuffer(VkCommandBuffer commandBuffer, VkQueue queue); + + VkCommandBuffer CreateSecondaryCommandBuffer(const char* debugName); + + const Ref& GetPhysicalDevice() const { return m_PhysicalDevice; } + VkDevice GetVulkanDevice() const { return m_LogicalDevice; } + private: + Ref GetThreadLocalCommandPool(); + Ref GetOrCreateThreadLocalCommandPool(); + private: + VkDevice m_LogicalDevice = nullptr; + Ref m_PhysicalDevice; + VkPhysicalDeviceFeatures m_EnabledFeatures; + + VkQueue m_GraphicsQueue; + VkQueue m_ComputeQueue; + + std::map> m_CommandPools; + bool m_EnableDebugMarkers = false; + + std::mutex m_GraphicsQueueMutex, m_ComputeQueueMutex; + }; + + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDeviceManager.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDeviceManager.cpp new file mode 100644 index 00000000..bee72539 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDeviceManager.cpp @@ -0,0 +1,803 @@ +/* +* Copyright (c) 2014-2021, NVIDIA CORPORATION. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +*/ + +/* +License for glfw + +Copyright (c) 2002-2006 Marcus Geelnard + +Copyright (c) 2006-2019 Camilla Lowy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. +*/ +#include "sepch.h" +#include "VulkanDeviceManager.h" + +// Define the Vulkan dynamic dispatcher - this needs to occur in exactly one cpp file in the program. +VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE + +namespace StarEngine { + + static std::vector stringSetToVector(const std::unordered_set& set) + { + std::vector ret; + for (const auto& s : set) + { + ret.push_back(s.c_str()); + } + + return ret; + } + + bool VulkanDeviceManager::createInstance() + { + if (!m_DeviceParams.headlessDevice) + { + if (!glfwVulkanSupported()) + { + SE_CORE_ERROR("GLFW reports that Vulkan is not supported. Perhaps missing a call to glfwInit()?"); + return false; + } + + // add any extensions required by GLFW + uint32_t glfwExtCount; + const char** glfwExt = glfwGetRequiredInstanceExtensions(&glfwExtCount); + SE_CORE_VERIFY(glfwExt); + + for (uint32_t i = 0; i < glfwExtCount; i++) + { + enabledExtensions.instance.insert(std::string(glfwExt[i])); + } + } + + // add instance extensions requested by the user + for (const std::string& name : m_DeviceParams.requiredVulkanInstanceExtensions) + { + enabledExtensions.instance.insert(name); + } + for (const std::string& name : m_DeviceParams.optionalVulkanInstanceExtensions) + { + optionalExtensions.instance.insert(name); + } + + // add layers requested by the user + for (const std::string& name : m_DeviceParams.requiredVulkanLayers) + { + enabledExtensions.layers.insert(name); + } + for (const std::string& name : m_DeviceParams.optionalVulkanLayers) + { + optionalExtensions.layers.insert(name); + } + + std::unordered_set requiredExtensions = enabledExtensions.instance; + + // figure out which optional extensions are supported + for (const auto& instanceExt : vk::enumerateInstanceExtensionProperties()) + { + const std::string name = instanceExt.extensionName; + if (optionalExtensions.instance.find(name) != optionalExtensions.instance.end()) + { + enabledExtensions.instance.insert(name); + } + + requiredExtensions.erase(name); + } + + if (!requiredExtensions.empty()) + { + std::stringstream ss; + ss << "Cannot create a Vulkan instance because the following required extension(s) are not supported:"; + for (const auto& ext : requiredExtensions) + ss << std::endl << " - " << ext; + + SE_CORE_ERROR("{}", ss.str().c_str()); + return false; + } + + SE_CORE_INFO("Enabled Vulkan instance extensions:"); + for (const auto& ext : enabledExtensions.instance) + { + SE_CORE_INFO(" {}", ext.c_str()); + } + + std::unordered_set requiredLayers = enabledExtensions.layers; + + for (const auto& layer : vk::enumerateInstanceLayerProperties()) + { + const std::string name = layer.layerName; + if (optionalExtensions.layers.find(name) != optionalExtensions.layers.end()) + { + enabledExtensions.layers.insert(name); + } + + requiredLayers.erase(name); + } + + if (!requiredLayers.empty()) + { + std::stringstream ss; + ss << "Cannot create a Vulkan instance because the following required layer(s) are not supported:"; + for (const auto& ext : requiredLayers) + ss << std::endl << " - " << ext; + + SE_CORE_ERROR("{}", ss.str().c_str()); + return false; + } + + SE_CORE_INFO("Enabled Vulkan layers:"); + for (const auto& layer : enabledExtensions.layers) + { + SE_CORE_INFO(" {}", layer.c_str()); + } + + auto instanceExtVec = stringSetToVector(enabledExtensions.instance); + auto layerVec = stringSetToVector(enabledExtensions.layers); + + auto applicationInfo = vk::ApplicationInfo(); + + // Query the Vulkan API version supported on the system to make sure we use at least 1.3 when that's present. + vk::Result res = vk::enumerateInstanceVersion(&applicationInfo.apiVersion); + + if (res != vk::Result::eSuccess) + { + SE_CORE_ERROR("Call to vkEnumerateInstanceVersion failed, error code = {}", nvrhi::vulkan::resultToString(VkResult(res))); + return false; + } + + const uint32_t minimumVulkanVersion = VK_MAKE_API_VERSION(0, 1, 3, 0); + + // Check if the Vulkan API version is sufficient. + if (applicationInfo.apiVersion < minimumVulkanVersion) + { + SE_CORE_ERROR("The Vulkan API version supported on the system ({}.{}.{}) is too low, at least {}.{}.{} is required.", + VK_API_VERSION_MAJOR(applicationInfo.apiVersion), VK_API_VERSION_MINOR(applicationInfo.apiVersion), VK_API_VERSION_PATCH(applicationInfo.apiVersion), + VK_API_VERSION_MAJOR(minimumVulkanVersion), VK_API_VERSION_MINOR(minimumVulkanVersion), VK_API_VERSION_PATCH(minimumVulkanVersion)); + return false; + } + + // Spec says: A non-zero variant indicates the API is a variant of the Vulkan API and applications will typically need to be modified to run against it. + if (VK_API_VERSION_VARIANT(applicationInfo.apiVersion) != 0) + { + SE_CORE_ERROR("The Vulkan API supported on the system uses an unexpected variant: {}.", VK_API_VERSION_VARIANT(applicationInfo.apiVersion)); + return false; + } + + // Create the vulkan instance + vk::InstanceCreateInfo info = vk::InstanceCreateInfo() + .setEnabledLayerCount(uint32_t(layerVec.size())) + .setPpEnabledLayerNames(layerVec.data()) + .setEnabledExtensionCount(uint32_t(instanceExtVec.size())) + .setPpEnabledExtensionNames(instanceExtVec.data()) + .setPApplicationInfo(&applicationInfo); + + res = vk::createInstance(&info, nullptr, &m_VulkanInstance); + if (res != vk::Result::eSuccess) + { + SE_CORE_ERROR("Failed to create a Vulkan instance, error code = {}", nvrhi::vulkan::resultToString(VkResult(res))); + return false; + } + + VULKAN_HPP_DEFAULT_DISPATCHER.init(m_VulkanInstance); + + return true; + } + + void VulkanDeviceManager::installDebugCallback() + { + auto info = vk::DebugReportCallbackCreateInfoEXT() + .setFlags(vk::DebugReportFlagBitsEXT::eError | + vk::DebugReportFlagBitsEXT::eWarning | + // vk::DebugReportFlagBitsEXT::eInformation | + vk::DebugReportFlagBitsEXT::ePerformanceWarning) + .setPfnCallback(vulkanDebugCallback) + .setPUserData(this); + + vk::Result res = m_VulkanInstance.createDebugReportCallbackEXT(&info, nullptr, &m_DebugReportCallback); + SE_CORE_VERIFY(res == vk::Result::eSuccess); + } + + bool VulkanDeviceManager::pickPhysicalDevice() + { + VkFormat requestedFormat = nvrhi::vulkan::convertFormat(m_DeviceParams.swapChainFormat); + vk::Extent2D requestedExtent(m_DeviceParams.backBufferWidth, m_DeviceParams.backBufferHeight); + + auto devices = m_VulkanInstance.enumeratePhysicalDevices(); + + int firstDevice = 0; + int lastDevice = int(devices.size()) - 1; + if (m_DeviceParams.adapterIndex >= 0) + { + if (m_DeviceParams.adapterIndex > lastDevice) + { + SE_CORE_ERROR("The specified Vulkan physical device {} does not exist.", m_DeviceParams.adapterIndex); + return false; + } + firstDevice = m_DeviceParams.adapterIndex; + lastDevice = m_DeviceParams.adapterIndex; + } + + // Start building an error message in case we cannot find a device. + std::stringstream errorStream; + errorStream << "Cannot find a Vulkan device that supports all the required extensions and properties."; + + // build a list of GPUs + std::vector discreteGPUs; + std::vector otherGPUs; + for (int deviceIndex = firstDevice; deviceIndex <= lastDevice; ++deviceIndex) + { + vk::PhysicalDevice const& dev = devices[deviceIndex]; + vk::PhysicalDeviceProperties prop = dev.getProperties(); + + errorStream << std::endl << prop.deviceName.data() << ":"; + + // check that all required device extensions are present + std::unordered_set requiredExtensions = enabledExtensions.device; + auto deviceExtensions = dev.enumerateDeviceExtensionProperties(); + for (const auto& ext : deviceExtensions) + { + requiredExtensions.erase(std::string(ext.extensionName.data())); + } + + bool deviceIsGood = true; + + if (!requiredExtensions.empty()) + { + // device is missing one or more required extensions + for (const auto& ext : requiredExtensions) + { + errorStream << std::endl << " - missing " << ext; + } + deviceIsGood = false; + } + + auto deviceFeatures = dev.getFeatures(); + if (!deviceFeatures.samplerAnisotropy) + { + // device is a toaster oven + errorStream << std::endl << " - does not support samplerAnisotropy"; + deviceIsGood = false; + } + if (!deviceFeatures.textureCompressionBC) + { + errorStream << std::endl << " - does not support textureCompressionBC"; + deviceIsGood = false; + } + + if (!FindQueueFamilies(dev)) + { + // device doesn't have all the queue families we need + errorStream << std::endl << " - does not support the necessary queue types"; + deviceIsGood = false; + } + + + if (!deviceIsGood) + continue; + + if (prop.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) + { + discreteGPUs.push_back(dev); + } + else + { + otherGPUs.push_back(dev); + } + } + + // pick the first discrete GPU if it exists, otherwise the first integrated GPU + if (!discreteGPUs.empty()) + { + m_VulkanPhysicalDevice = discreteGPUs[0]; + return true; + } + + if (!otherGPUs.empty()) + { + m_VulkanPhysicalDevice = otherGPUs[0]; + return true; + } + + SE_CORE_ERROR("{}", errorStream.str().c_str()); + + return false; + } + + bool VulkanDeviceManager::FindQueueFamilies(vk::PhysicalDevice physicalDevice) + { + auto props = physicalDevice.getQueueFamilyProperties(); + + for (int i = 0; i < int(props.size()); i++) + { + const auto& queueFamily = props[i]; + + if (m_QueueFamilyIndices.Graphics == -1) + { + if (queueFamily.queueCount > 0 && + (queueFamily.queueFlags & vk::QueueFlagBits::eGraphics)) + { + m_QueueFamilyIndices.Graphics = i; + } + } + + if (m_QueueFamilyIndices.Compute == -1) + { + if (queueFamily.queueCount > 0 && + (queueFamily.queueFlags & vk::QueueFlagBits::eCompute) && + !(queueFamily.queueFlags & vk::QueueFlagBits::eGraphics)) + { + m_QueueFamilyIndices.Compute = i; + } + } + + if (m_QueueFamilyIndices.Transfer == -1) + { + if (queueFamily.queueCount > 0 && + (queueFamily.queueFlags & vk::QueueFlagBits::eTransfer) && + !(queueFamily.queueFlags & vk::QueueFlagBits::eCompute) && + !(queueFamily.queueFlags & vk::QueueFlagBits::eGraphics)) + { + m_QueueFamilyIndices.Transfer = i; + } + } + + if (m_QueueFamilyIndices.Present == -1) + { + if (queueFamily.queueCount > 0 && + glfwGetPhysicalDevicePresentationSupport(m_VulkanInstance, physicalDevice, i)) + { + m_QueueFamilyIndices.Present = i; + } + } + } + + if (m_QueueFamilyIndices.Graphics == -1 || + m_QueueFamilyIndices.Present == -1 && !m_DeviceParams.headlessDevice || + (m_QueueFamilyIndices.Compute == -1 && m_DeviceParams.enableComputeQueue) || + (m_QueueFamilyIndices.Transfer == -1 && m_DeviceParams.enableCopyQueue)) + { + return false; + } + + return true; + } + + bool VulkanDeviceManager::createDevice() + { + // figure out which optional extensions are supported + auto deviceExtensions = m_VulkanPhysicalDevice.enumerateDeviceExtensionProperties(); + for (const auto& ext : deviceExtensions) + { + const std::string name = ext.extensionName; + if (optionalExtensions.device.find(name) != optionalExtensions.device.end()) + { + if (name == VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME && m_DeviceParams.headlessDevice) + continue; + + enabledExtensions.device.insert(name); + } + + if (m_DeviceParams.enableRayTracingExtensions && m_RayTracingExtensions.find(name) != m_RayTracingExtensions.end()) + { + enabledExtensions.device.insert(name); + } + } + + if (!m_DeviceParams.headlessDevice) + { + enabledExtensions.device.insert(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + } + + const vk::PhysicalDeviceProperties physicalDeviceProperties = m_VulkanPhysicalDevice.getProperties(); + m_RendererString = std::string(physicalDeviceProperties.deviceName.data()); + + bool accelStructSupported = false; + bool rayPipelineSupported = false; + bool rayQuerySupported = false; + bool meshletsSupported = false; + bool vrsSupported = false; + bool synchronization2Supported = false; + bool maintenance4Supported = false; + + SE_CORE_INFO("Enabled Vulkan device extensions:"); + for (const auto& ext : enabledExtensions.device) + { + SE_CORE_INFO(" {}", ext.c_str()); + + if (ext == VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) + accelStructSupported = true; + else if (ext == VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME) + rayPipelineSupported = true; + else if (ext == VK_KHR_RAY_QUERY_EXTENSION_NAME) + rayQuerySupported = true; + else if (ext == VK_NV_MESH_SHADER_EXTENSION_NAME) + meshletsSupported = true; + else if (ext == VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME) + vrsSupported = true; + else if (ext == VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME) + synchronization2Supported = true; + else if (ext == VK_KHR_MAINTENANCE_4_EXTENSION_NAME) + maintenance4Supported = true; + else if (ext == VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME) + m_SwapChainMutableFormatSupported = true; + } + +#define APPEND_EXTENSION(condition, desc) if (condition) { (desc).pNext = pNext; pNext = &(desc); } // NOLINT(cppcoreguidelines-macro-usage) + void* pNext = nullptr; + + vk::PhysicalDeviceFeatures2 physicalDeviceFeatures2; + // Determine support for Buffer Device Address, the Vulkan 1.2 way + auto bufferDeviceAddressFeatures = vk::PhysicalDeviceBufferDeviceAddressFeatures(); + // Determine support for maintenance4 + auto maintenance4Features = vk::PhysicalDeviceMaintenance4Features(); + + // Put the user-provided extension structure at the end of the chain + pNext = m_DeviceParams.physicalDeviceFeatures2Extensions; + APPEND_EXTENSION(true, bufferDeviceAddressFeatures); + APPEND_EXTENSION(maintenance4Supported, maintenance4Features); + + physicalDeviceFeatures2.pNext = pNext; + m_VulkanPhysicalDevice.getFeatures2(&physicalDeviceFeatures2); + + std::unordered_set uniqueQueueFamilies = { + m_QueueFamilyIndices.Graphics }; + + if (!m_DeviceParams.headlessDevice) + uniqueQueueFamilies.insert(m_QueueFamilyIndices.Present); + + if (m_DeviceParams.enableComputeQueue) + uniqueQueueFamilies.insert(m_QueueFamilyIndices.Compute); + + if (m_DeviceParams.enableCopyQueue) + uniqueQueueFamilies.insert(m_QueueFamilyIndices.Transfer); + + float priority = 1.f; + std::vector queueDesc; + queueDesc.reserve(uniqueQueueFamilies.size()); + for (int queueFamily : uniqueQueueFamilies) + { + queueDesc.push_back(vk::DeviceQueueCreateInfo() + .setQueueFamilyIndex(queueFamily) + .setQueueCount(1) + .setPQueuePriorities(&priority)); + } + + auto accelStructFeatures = vk::PhysicalDeviceAccelerationStructureFeaturesKHR() + .setAccelerationStructure(true); + auto rayPipelineFeatures = vk::PhysicalDeviceRayTracingPipelineFeaturesKHR() + .setRayTracingPipeline(true) + .setRayTraversalPrimitiveCulling(true); + auto rayQueryFeatures = vk::PhysicalDeviceRayQueryFeaturesKHR() + .setRayQuery(true); + auto meshletFeatures = vk::PhysicalDeviceMeshShaderFeaturesNV() + .setTaskShader(true) + .setMeshShader(true); + auto vrsFeatures = vk::PhysicalDeviceFragmentShadingRateFeaturesKHR() + .setPipelineFragmentShadingRate(true) + .setPrimitiveFragmentShadingRate(true) + .setAttachmentFragmentShadingRate(true); + auto vulkan13features = vk::PhysicalDeviceVulkan13Features() + .setSynchronization2(synchronization2Supported) + .setMaintenance4(maintenance4Features.maintenance4); + + pNext = nullptr; + APPEND_EXTENSION(accelStructSupported, accelStructFeatures) + APPEND_EXTENSION(rayPipelineSupported, rayPipelineFeatures) + APPEND_EXTENSION(rayQuerySupported, rayQueryFeatures) + APPEND_EXTENSION(meshletsSupported, meshletFeatures) + APPEND_EXTENSION(vrsSupported, vrsFeatures) + APPEND_EXTENSION(physicalDeviceProperties.apiVersion >= VK_API_VERSION_1_3, vulkan13features) + APPEND_EXTENSION(physicalDeviceProperties.apiVersion < VK_API_VERSION_1_3 && maintenance4Supported, maintenance4Features); +#undef APPEND_EXTENSION + + auto deviceFeatures = vk::PhysicalDeviceFeatures() + .setShaderImageGatherExtended(true) + .setSamplerAnisotropy(true) + .setTessellationShader(true) + .setTextureCompressionBC(true) + .setGeometryShader(true) + .setImageCubeArray(true) + .setDualSrcBlend(true) + .setIndependentBlend(true) + .setFillModeNonSolid(true) + .setWideLines(true); + + auto vulkan12features = vk::PhysicalDeviceVulkan12Features() + .setDescriptorIndexing(true) + .setRuntimeDescriptorArray(true) + .setDescriptorBindingPartiallyBound(true) + .setDescriptorBindingVariableDescriptorCount(true) + .setTimelineSemaphore(true) + .setShaderSampledImageArrayNonUniformIndexing(true) + .setBufferDeviceAddress(bufferDeviceAddressFeatures.bufferDeviceAddress) + .setPNext(pNext); + + auto layerVec = stringSetToVector(enabledExtensions.layers); + auto extVec = stringSetToVector(enabledExtensions.device); + + auto deviceDesc = vk::DeviceCreateInfo() + .setPQueueCreateInfos(queueDesc.data()) + .setQueueCreateInfoCount(uint32_t(queueDesc.size())) + .setPEnabledFeatures(&deviceFeatures) + .setEnabledExtensionCount(uint32_t(extVec.size())) + .setPpEnabledExtensionNames(extVec.data()) + .setEnabledLayerCount(uint32_t(layerVec.size())) + .setPpEnabledLayerNames(layerVec.data()) + .setPNext(&vulkan12features); + + if (m_DeviceParams.deviceCreateInfoCallback) + m_DeviceParams.deviceCreateInfoCallback(deviceDesc); + + const vk::Result res = m_VulkanPhysicalDevice.createDevice(&deviceDesc, nullptr, &m_VulkanDevice); + if (res != vk::Result::eSuccess) + { + SE_CORE_ERROR("Failed to create a Vulkan physical device, error code = {}", nvrhi::vulkan::resultToString(VkResult(res))); + return false; + } + + m_VulkanDevice.getQueue(m_QueueFamilyIndices.Graphics, 0, &m_GraphicsQueue); + if (m_DeviceParams.enableComputeQueue) + m_VulkanDevice.getQueue(m_QueueFamilyIndices.Compute, 0, &m_ComputeQueue); + if (m_DeviceParams.enableCopyQueue) + m_VulkanDevice.getQueue(m_QueueFamilyIndices.Transfer, 0, &m_TransferQueue); + if (!m_DeviceParams.headlessDevice) + m_VulkanDevice.getQueue(m_QueueFamilyIndices.Present, 0, &m_PresentQueue); + + VULKAN_HPP_DEFAULT_DISPATCHER.init(m_VulkanDevice); + + // remember the bufferDeviceAddress feature enablement + m_BufferDeviceAddressSupported = vulkan12features.bufferDeviceAddress; + + SE_CORE_INFO("Created Vulkan device: {}", m_RendererString.c_str()); + + return true; + } + +#define CHECK(a) if (!(a)) { return false; } + + bool VulkanDeviceManager::CreateInstanceInternal() + { + if (m_DeviceParams.enableDebugRuntime) + { + enabledExtensions.instance.insert("VK_EXT_debug_report"); + enabledExtensions.layers.insert("VK_LAYER_KHRONOS_validation"); + } + + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = + m_dynamicLoader.getProcAddress("vkGetInstanceProcAddr"); + VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); + + return createInstance(); + } + + bool VulkanDeviceManager::EnumerateAdapters(std::vector& outAdapters) + { + if (!m_VulkanInstance) + return false; + + std::vector devices = m_VulkanInstance.enumeratePhysicalDevices(); + outAdapters.clear(); + + for (auto physicalDevice : devices) + { + vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); + + AdapterInfo adapterInfo; + adapterInfo.name = properties.deviceName.data(); + adapterInfo.vendorID = properties.vendorID; + adapterInfo.deviceID = properties.deviceID; + adapterInfo.vkPhysicalDevice = physicalDevice; + adapterInfo.dedicatedVideoMemory = 0; + + // Go through the memory types to figure out the amount of VRAM on this physical device. + vk::PhysicalDeviceMemoryProperties memoryProperties = physicalDevice.getMemoryProperties(); + for (uint32_t heapIndex = 0; heapIndex < memoryProperties.memoryHeapCount; ++heapIndex) + { + vk::MemoryHeap const& heap = memoryProperties.memoryHeaps[heapIndex]; + if (heap.flags & vk::MemoryHeapFlagBits::eDeviceLocal) + { + adapterInfo.dedicatedVideoMemory += heap.size; + } + } + + outAdapters.push_back(std::move(adapterInfo)); + } + + return true; + } + + bool VulkanDeviceManager::CreateDevice() + { + if (m_DeviceParams.enableDebugRuntime) + { + installDebugCallback(); + } + + // add device extensions requested by the user + for (const std::string& name : m_DeviceParams.requiredVulkanDeviceExtensions) + { + enabledExtensions.device.insert(name); + } + for (const std::string& name : m_DeviceParams.optionalVulkanDeviceExtensions) + { + optionalExtensions.device.insert(name); + } + + if (!m_DeviceParams.headlessDevice) + { + // Need to adjust the swap chain format before creating the device because it affects physical device selection + if (m_DeviceParams.swapChainFormat == nvrhi::Format::SRGBA8_UNORM) + m_DeviceParams.swapChainFormat = nvrhi::Format::SBGRA8_UNORM; + else if (m_DeviceParams.swapChainFormat == nvrhi::Format::RGBA8_UNORM) + m_DeviceParams.swapChainFormat = nvrhi::Format::BGRA8_UNORM; + } + CHECK(pickPhysicalDevice()) + CHECK(FindQueueFamilies(m_VulkanPhysicalDevice)) + CHECK(createDevice()) + + auto vecInstanceExt = stringSetToVector(enabledExtensions.instance); + auto vecLayers = stringSetToVector(enabledExtensions.layers); + auto vecDeviceExt = stringSetToVector(enabledExtensions.device); + + nvrhi::vulkan::DeviceDesc deviceDesc; + deviceDesc.errorCB = &DefaultMessageCallback::GetInstance(); + deviceDesc.instance = m_VulkanInstance; + deviceDesc.physicalDevice = m_VulkanPhysicalDevice; + deviceDesc.device = m_VulkanDevice; + deviceDesc.graphicsQueue = m_GraphicsQueue; + deviceDesc.graphicsQueueIndex = m_QueueFamilyIndices.Graphics; + if (m_DeviceParams.enableComputeQueue) + { + deviceDesc.computeQueue = m_ComputeQueue; + deviceDesc.computeQueueIndex = m_QueueFamilyIndices.Compute; + } + if (m_DeviceParams.enableCopyQueue) + { + deviceDesc.transferQueue = m_TransferQueue; + deviceDesc.transferQueueIndex = m_QueueFamilyIndices.Transfer; + } + deviceDesc.instanceExtensions = vecInstanceExt.data(); + deviceDesc.numInstanceExtensions = vecInstanceExt.size(); + deviceDesc.deviceExtensions = vecDeviceExt.data(); + deviceDesc.numDeviceExtensions = vecDeviceExt.size(); + deviceDesc.bufferDeviceAddressSupported = m_BufferDeviceAddressSupported; + + m_NvrhiDevice = nvrhi::vulkan::createDevice(deviceDesc); + + if (m_DeviceParams.enableNvrhiValidationLayer) + { + m_ValidationLayer = nvrhi::validation::createValidationLayer(m_NvrhiDevice); + } + + return true; + } + + bool VulkanDeviceManager::InitSurfaceCapabilities(uint64_t surfaceHandle) + { +#if TODO + vk::SurfaceKHR windowSurface = (VkSurfaceKHR)surfaceHandle; + + if (windowSurface) + { + // check that this device supports our intended swap chain creation parameters + auto surfaceCaps = dev.getSurfaceCapabilitiesKHR(windowSurface); + auto surfaceFmts = dev.getSurfaceFormatsKHR(windowSurface); + auto surfacePModes = dev.getSurfacePresentModesKHR(windowSurface); + + if (surfaceCaps.minImageCount > m_DeviceParams.swapChainBufferCount || + (surfaceCaps.maxImageCount < m_DeviceParams.swapChainBufferCount && surfaceCaps.maxImageCount > 0)) + { + errorStream << std::endl << " - cannot support the requested swap chain image count:"; + errorStream << " requested " << m_DeviceParams.swapChainBufferCount << ", available " << surfaceCaps.minImageCount << " - " << surfaceCaps.maxImageCount; + deviceIsGood = false; + } + + if (surfaceCaps.minImageExtent.width > requestedExtent.width || + surfaceCaps.minImageExtent.height > requestedExtent.height || + surfaceCaps.maxImageExtent.width < requestedExtent.width || + surfaceCaps.maxImageExtent.height < requestedExtent.height) + { + errorStream << std::endl << " - cannot support the requested swap chain size:"; + errorStream << " requested " << requestedExtent.width << "x" << requestedExtent.height << ", "; + errorStream << " available " << surfaceCaps.minImageExtent.width << "x" << surfaceCaps.minImageExtent.height; + errorStream << " - " << surfaceCaps.maxImageExtent.width << "x" << surfaceCaps.maxImageExtent.height; + deviceIsGood = false; + } + + bool surfaceFormatPresent = false; + for (const vk::SurfaceFormatKHR& surfaceFmt : surfaceFmts) + { + if (surfaceFmt.format == vk::Format(requestedFormat)) + { + surfaceFormatPresent = true; + break; + } + } + + if (!surfaceFormatPresent) + { + // can't create a swap chain using the format requested + errorStream << std::endl << " - does not support the requested swap chain format"; + deviceIsGood = false; + } + + // check that we can present from the graphics queue + uint32_t canPresent = dev.getSurfaceSupportKHR(m_QueueFamilyIndices.Graphics, windowSurface); + if (!canPresent) + { + errorStream << std::endl << " - cannot present"; + deviceIsGood = false; + } + } +#endif + return true; + } + +#undef CHECK + + void VulkanDeviceManager::DestroyDevice() + { + m_NvrhiDevice = nullptr; + m_ValidationLayer = nullptr; + m_RendererString.clear(); + + if (m_VulkanDevice) + { + m_VulkanDevice.destroy(); + m_VulkanDevice = nullptr; + } + + if (m_DebugReportCallback) + { + m_VulkanInstance.destroyDebugReportCallbackEXT(m_DebugReportCallback); + } + + if (m_VulkanInstance) + { + m_VulkanInstance.destroy(); + m_VulkanInstance = nullptr; + } + } + + DeviceManager* DeviceManager::CreateVK(GLFWwindow* windowHandle) + { + return new VulkanDeviceManager(windowHandle); + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDeviceManager.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDeviceManager.h new file mode 100644 index 00000000..c303c174 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDeviceManager.h @@ -0,0 +1,255 @@ +/* +* Copyright (c) 2014-2021, NVIDIA CORPORATION. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining a +* copy of this software and associated documentation files (the "Software"), +* to deal in the Software without restriction, including without limitation +* the rights to use, copy, modify, merge, publish, distribute, sublicense, +* and/or sell copies of the Software, and to permit persons to whom the +* Software is furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +* DEALINGS IN THE SOFTWARE. +*/ + +/* +License for glfw + +Copyright (c) 2002-2006 Marcus Geelnard + +Copyright (c) 2006-2019 Camilla Lowy + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. +*/ +#include "StarEngine/Renderer/DeviceManager.h" +#include "StarEngine/Core/Window.h" +#include "StarEngine/Debug/Profiler.h" + +#include +#include +#include + +#include +#include + +#include + +#include "StarEngine/Core/Log.h" + +namespace StarEngine { + + class VulkanDeviceManager : public DeviceManager + { + public: + struct QueueFamilyIndices + { + int32_t Graphics = -1; + int32_t Compute = -1; + int32_t Transfer = -1; + int32_t Present = -1; + }; + public: + VulkanDeviceManager(GLFWwindow* windowHandle) + { + m_WindowHandle = windowHandle; + } + + [[nodiscard]] nvrhi::IDevice* GetDevice() const override + { + if (m_ValidationLayer) + return m_ValidationLayer; + + return m_NvrhiDevice; + } + + [[nodiscard]] nvrhi::GraphicsAPI GetGraphicsAPI() const override + { + return nvrhi::GraphicsAPI::VULKAN; + } + + bool EnumerateAdapters(std::vector& outAdapters) override; + + vk::Instance GetVulkanInstance() const { return m_VulkanInstance; } + protected: + virtual bool CreateInstanceInternal() override; + virtual bool CreateDevice() override; + virtual bool InitSurfaceCapabilities(uint64_t surfaceHandle) override; + virtual void DestroyDevice() override; + + const char* GetRendererString() const override + { + return m_RendererString.c_str(); + } + + bool IsVulkanInstanceExtensionEnabled(const char* extensionName) const override + { + return enabledExtensions.instance.find(extensionName) != enabledExtensions.instance.end(); + } + + bool IsVulkanDeviceExtensionEnabled(const char* extensionName) const override + { + return enabledExtensions.device.find(extensionName) != enabledExtensions.device.end(); + } + + bool IsVulkanLayerEnabled(const char* layerName) const override + { + return enabledExtensions.layers.find(layerName) != enabledExtensions.layers.end(); + } + + void GetEnabledVulkanInstanceExtensions(std::vector& extensions) const override + { + for (const auto& ext : enabledExtensions.instance) + extensions.push_back(ext); + } + + void GetEnabledVulkanDeviceExtensions(std::vector& extensions) const override + { + for (const auto& ext : enabledExtensions.device) + extensions.push_back(ext); + } + + void GetEnabledVulkanLayers(std::vector& layers) const override + { + for (const auto& ext : enabledExtensions.layers) + layers.push_back(ext); + } + + private: + bool createInstance(); + bool createWindowSurface(); + void installDebugCallback(); + bool pickPhysicalDevice(); + bool FindQueueFamilies(vk::PhysicalDevice physicalDevice); + bool createDevice(); + bool createSwapChain(); + void destroySwapChain(); + + struct VulkanExtensionSet + { + std::unordered_set instance; + std::unordered_set layers; + std::unordered_set device; + }; + + // minimal set of required extensions + VulkanExtensionSet enabledExtensions = { + // instance + { + VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME + }, + // layers + { }, + // device + { + VK_KHR_MAINTENANCE1_EXTENSION_NAME + }, + }; + + // optional extensions + VulkanExtensionSet optionalExtensions = { + // instance + { + VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME, + VK_EXT_DEBUG_UTILS_EXTENSION_NAME + }, + // layers + { }, + // device + { + VK_EXT_DEBUG_MARKER_EXTENSION_NAME, + VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME, + VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, + VK_NV_MESH_SHADER_EXTENSION_NAME, + VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME, + VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, + VK_KHR_MAINTENANCE_4_EXTENSION_NAME, + VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME + }, + }; + + std::unordered_set m_RayTracingExtensions = { + VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME, + VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME, + VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME, + VK_KHR_RAY_QUERY_EXTENSION_NAME, + VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME + }; + + std::string m_RendererString; + + vk::Instance m_VulkanInstance; + vk::DebugReportCallbackEXT m_DebugReportCallback; + + vk::PhysicalDevice m_VulkanPhysicalDevice; + + QueueFamilyIndices m_QueueFamilyIndices; + + vk::Device m_VulkanDevice; + vk::Queue m_GraphicsQueue; + vk::Queue m_ComputeQueue; + vk::Queue m_TransferQueue; + vk::Queue m_PresentQueue; + + nvrhi::vulkan::DeviceHandle m_NvrhiDevice; + nvrhi::DeviceHandle m_ValidationLayer; + + bool m_SwapChainMutableFormatSupported = false; + bool m_BufferDeviceAddressSupported = false; + + vk::detail::DynamicLoader m_dynamicLoader; + + private: + static VKAPI_ATTR VkBool32 VKAPI_CALL vulkanDebugCallback( + VkDebugReportFlagsEXT flags, + VkDebugReportObjectTypeEXT objType, + uint64_t obj, + size_t location, + int32_t code, + const char* layerPrefix, + const char* msg, + void* userData) + { + const VulkanDeviceManager* manager = (const VulkanDeviceManager*)userData; + + if (manager) + { + const auto& ignored = manager->m_DeviceParams.ignoredVulkanValidationMessageLocations; + const auto found = std::find(ignored.begin(), ignored.end(), location); + if (found != ignored.end()) + return VK_FALSE; + } + + SE_CORE_WARN_TAG("Renderer", "[Vulkan: location=0x{0:x} code={1}, layerPrefix='{2}'] {3}\n", location, code, layerPrefix, msg); + + return VK_FALSE; + } + + friend class VulkanSwapChain; + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDiagnostics.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDiagnostics.cpp new file mode 100644 index 00000000..a7faddae --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDiagnostics.cpp @@ -0,0 +1,24 @@ +#include "sepch.h" +#include "VulkanDiagnostics.h" + +#include "StarEngine/Platform/Vulkan/VulkanContext.h" + +namespace StarEngine::Utils { + + static std::vector s_CheckpointStorage(1024); + static uint32_t s_CheckpointStorageIndex = 0; + + void SetVulkanCheckpoint(VkCommandBuffer commandBuffer, const std::string& data) + { + const bool supported = VulkanContext::GetCurrentDevice()->GetPhysicalDevice()->IsExtensionSupported(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME); + if (!supported) + return; + + s_CheckpointStorageIndex = (s_CheckpointStorageIndex + 1) % 1024; + VulkanCheckpointData& checkpoint = s_CheckpointStorage[s_CheckpointStorageIndex]; + memset(checkpoint.Data, 0, sizeof(checkpoint.Data)); + strcpy(checkpoint.Data, data.data()); + vkCmdSetCheckpointNV(commandBuffer, &checkpoint); + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDiagnostics.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDiagnostics.h new file mode 100644 index 00000000..500da491 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanDiagnostics.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "Vulkan.h" + +namespace StarEngine::Utils { + + struct VulkanCheckpointData + { + char Data[64 + 1] {}; + }; + + void SetVulkanCheckpoint(VkCommandBuffer commandBuffer, const std::string& data); + +} + diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanImGuiLayer.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanImGuiLayer.cpp new file mode 100644 index 00000000..057fe6b5 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanImGuiLayer.cpp @@ -0,0 +1,305 @@ +#include "sepch.h" +#include "VulkanImGuiLayer.h" + +#include "imgui.h" +#include "StarEngine/Core/Input.h" +#include "StarEngine/ImGui/ImGuizmo.h" +#include "StarEngine/ImGui/ImGuiFonts.h" + +#ifndef IMGUI_IMPL_API +#define IMGUI_IMPL_API +#endif +#include "backends/imgui_impl_glfw.h" + +#include "StarEngine/Core/Application.h" +#include + +#include "StarEngine/Editor/FontAwesome.h" + +#include "StarEngine/Renderer/Renderer.h" + +#include "StarEngine/Platform/Vulkan/VulkanContext.h" + +namespace StarEngine { + + static std::vector s_ImGuiCommandBuffers; + + VulkanImGuiLayer::VulkanImGuiLayer() + { + } + + VulkanImGuiLayer::VulkanImGuiLayer(const std::string& name) + { + + } + + VulkanImGuiLayer::~VulkanImGuiLayer() + { + } + + void VulkanImGuiLayer::OnAttach() + { + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); (void)io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking + io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows + + // Configure Fonts + { + UI::FontConfiguration robotoBold; + robotoBold.FontName = "Bold"; + robotoBold.FilePath = "Resources/Fonts/Roboto/Roboto-Bold.ttf"; + robotoBold.Size = 18.0f; + UI::Fonts::Add(robotoBold); + + UI::FontConfiguration robotoLarge; + robotoLarge.FontName = "Large"; + robotoLarge.FilePath = "Resources/Fonts/Roboto/Roboto-Regular.ttf"; + robotoLarge.Size = 24.0f; + UI::Fonts::Add(robotoLarge); + + UI::FontConfiguration robotoDefault; + robotoDefault.FontName = "Default"; + robotoDefault.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoDefault.Size = 15.0f; + UI::Fonts::Add(robotoDefault, true); + + static const ImWchar s_FontAwesomeRanges[] = { SE_ICON_MIN, SE_ICON_MAX, 0 }; + UI::FontConfiguration fontAwesome; + fontAwesome.FontName = "FontAwesome"; + fontAwesome.FilePath = "Resources/Fonts/FontAwesome/fontawesome-webfont.ttf"; + fontAwesome.Size = 16.0f; + fontAwesome.GlyphRanges = s_FontAwesomeRanges; + fontAwesome.MergeWithLast = true; + UI::Fonts::Add(fontAwesome); + + UI::FontConfiguration robotoMedium; + robotoMedium.FontName = "Medium"; + robotoMedium.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoMedium.Size = 18.0f; + UI::Fonts::Add(robotoMedium); + + UI::FontConfiguration robotoSmall; + robotoSmall.FontName = "Small"; + robotoSmall.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoSmall.Size = 12.0f; + UI::Fonts::Add(robotoSmall); + + UI::FontConfiguration robotoExtraSmall; + robotoExtraSmall.FontName = "ExtraSmall"; + robotoExtraSmall.FilePath = "Resources/Fonts/Roboto/Roboto-SemiMedium.ttf"; + robotoExtraSmall.Size = 10.0f; + UI::Fonts::Add(robotoExtraSmall); + + UI::FontConfiguration robotoBoldTitle; + robotoBoldTitle.FontName = "BoldTitle"; + robotoBoldTitle.FilePath = "Resources/Fonts/Roboto/Roboto-Bold.ttf"; + robotoBoldTitle.Size = 16.0f; + UI::Fonts::Add(robotoBoldTitle); + } + + // Setup Dear ImGui style + ImGui::StyleColorsDark(); + SetDarkThemeV2Colors(); + + // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones. + ImGuiStyle& style = ImGui::GetStyle(); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + style.WindowRounding = 0.0f; + style.Colors[ImGuiCol_WindowBg].w = 1.0f; + } + style.Colors[ImGuiCol_WindowBg] = ImVec4(0.15f, 0.15f, 0.15f, style.Colors[ImGuiCol_WindowBg].w); + +#if OLD + VulkanImGuiLayer* instance = this; + Renderer::Submit([instance]() + { + Application& app = Application::Get(); + GLFWwindow* window = static_cast(app.GetWindow().GetNativeWindow()); + + auto vulkanContext = VulkanContext::Get(); + auto device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + VkDescriptorPool descriptorPool; + + // Create Descriptor Pool + VkDescriptorPoolSize pool_sizes[] = + { + { VK_DESCRIPTOR_TYPE_SAMPLER, 100 }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 100 }, + { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 100 }, + { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 100 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 100 }, + { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 100 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 100 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 100 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 100 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 100 }, + { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 100 } + }; + VkDescriptorPoolCreateInfo pool_info = {}; + pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + pool_info.maxSets = 100 * IM_ARRAYSIZE(pool_sizes); + pool_info.poolSizeCount = (uint32_t)IM_ARRAYSIZE(pool_sizes); + pool_info.pPoolSizes = pool_sizes; + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &pool_info, nullptr, &descriptorPool)); + + // Setup Platform/Renderer bindings + ImGui_ImplGlfw_InitForVulkan(window, true); + ImGui_ImplVulkan_InitInfo init_info = {}; + init_info.Instance = VulkanContext::GetInstance(); + init_info.PhysicalDevice = VulkanContext::GetCurrentDevice()->GetPhysicalDevice()->GetVulkanPhysicalDevice(); + init_info.Device = device; + init_info.QueueFamily = VulkanContext::GetCurrentDevice()->GetPhysicalDevice()->GetQueueFamilyIndices().Graphics; + init_info.Queue = VulkanContext::GetCurrentDevice()->GetGraphicsQueue(); + init_info.PipelineCache = nullptr; + init_info.DescriptorPool = descriptorPool; + init_info.Allocator = nullptr; + init_info.MinImageCount = 2; + VulkanSwapChain& swapChain = Application::Get().GetWindow().GetSwapChain(); + init_info.ImageCount = swapChain.GetImageCount(); + init_info.CheckVkResultFn = Utils::VulkanCheckResult; + ImGui_ImplVulkan_Init(&init_info, swapChain.GetRenderPass()); + + // Upload Fonts + { + // Use any command queue + + VkCommandBuffer commandBuffer = vulkanContext->GetCurrentDevice()->GetCommandBuffer(true); + ImGui_ImplVulkan_CreateFontsTexture(commandBuffer); + vulkanContext->GetCurrentDevice()->FlushCommandBuffer(commandBuffer); + + VK_CHECK_RESULT(vkDeviceWaitIdle(device)); + ImGui_ImplVulkan_DestroyFontUploadObjects(); + } + + uint32_t framesInFlight = Renderer::GetConfig().FramesInFlight; + s_ImGuiCommandBuffers.resize(framesInFlight); + for (uint32_t i = 0; i < framesInFlight; i++) + s_ImGuiCommandBuffers[i] = VulkanContext::GetCurrentDevice()->CreateSecondaryCommandBuffer("ImGuiSecondaryCoommandBuffer"); + }); +#endif + } + + void VulkanImGuiLayer::OnDetach() + { + Renderer::Submit([]() + { + auto device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + VK_CHECK_RESULT(vkDeviceWaitIdle(device)); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + }); + } + + void VulkanImGuiLayer::Begin() + { + ImGui::SetMouseCursor(Input::GetCursorMode() == CursorMode::Normal ? ImGuiMouseCursor_Arrow : ImGuiMouseCursor_None); + + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + ImGuizmo::BeginFrame(); + } + + void VulkanImGuiLayer::End() + { +#if OLD + ImGui::Render(); + + VulkanSwapChain& swapChain = Application::Get().GetWindow().GetSwapChain(); + + VkClearValue clearValues[2]; + clearValues[0].color = { {0.1f, 0.1f,0.1f, 1.0f} }; + clearValues[1].depthStencil = { 1.0f, 0 }; + + uint32_t width = swapChain.GetWidth(); + uint32_t height = swapChain.GetHeight(); + + uint32_t commandBufferIndex = swapChain.GetCurrentBufferIndex(); + + VkCommandBufferBeginInfo drawCmdBufInfo = {}; + drawCmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + drawCmdBufInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + drawCmdBufInfo.pNext = nullptr; + + VkCommandBuffer drawCommandBuffer = swapChain.GetCurrentDrawCommandBuffer(); + VK_CHECK_RESULT(vkBeginCommandBuffer(drawCommandBuffer, &drawCmdBufInfo)); + + VkRenderPassBeginInfo renderPassBeginInfo = {}; + renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassBeginInfo.pNext = nullptr; + renderPassBeginInfo.renderPass = swapChain.GetRenderPass(); + renderPassBeginInfo.renderArea.offset.x = 0; + renderPassBeginInfo.renderArea.offset.y = 0; + renderPassBeginInfo.renderArea.extent.width = width; + renderPassBeginInfo.renderArea.extent.height = height; + renderPassBeginInfo.clearValueCount = 2; // Color + depth + renderPassBeginInfo.pClearValues = clearValues; + renderPassBeginInfo.framebuffer = swapChain.GetCurrentFramebuffer(); + + vkCmdBeginRenderPass(drawCommandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS); + + VkCommandBufferInheritanceInfo inheritanceInfo = {}; + inheritanceInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO; + inheritanceInfo.renderPass = swapChain.GetRenderPass(); + inheritanceInfo.framebuffer = swapChain.GetCurrentFramebuffer(); + + VkCommandBufferBeginInfo cmdBufInfo = {}; + cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + cmdBufInfo.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT; + cmdBufInfo.pInheritanceInfo = &inheritanceInfo; + + VK_CHECK_RESULT(vkBeginCommandBuffer(s_ImGuiCommandBuffers[commandBufferIndex], &cmdBufInfo)); + + VkViewport viewport = {}; + viewport.x = 0.0f; + viewport.y = (float)height; + viewport.height = -(float)height; + viewport.width = (float)width; + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + vkCmdSetViewport(s_ImGuiCommandBuffers[commandBufferIndex], 0, 1, &viewport); + + VkRect2D scissor = {}; + scissor.extent.width = width; + scissor.extent.height = height; + scissor.offset.x = 0; + scissor.offset.y = 0; + vkCmdSetScissor(s_ImGuiCommandBuffers[commandBufferIndex], 0, 1, &scissor); + + ImDrawData* main_draw_data = ImGui::GetDrawData(); + ImGui_ImplVulkan_RenderDrawData(main_draw_data, s_ImGuiCommandBuffers[commandBufferIndex]); + + VK_CHECK_RESULT(vkEndCommandBuffer(s_ImGuiCommandBuffers[commandBufferIndex])); + + std::vector commandBuffers; + commandBuffers.push_back(s_ImGuiCommandBuffers[commandBufferIndex]); + + vkCmdExecuteCommands(drawCommandBuffer, uint32_t(commandBuffers.size()), commandBuffers.data()); + + vkCmdEndRenderPass(drawCommandBuffer); + + VK_CHECK_RESULT(vkEndCommandBuffer(drawCommandBuffer)); + + ImGuiIO& io = ImGui::GetIO(); (void)io; + // Update and Render additional Platform Windows + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); + } +#endif + } + + void VulkanImGuiLayer::OnImGuiRender() + { + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanImGuiLayer.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanImGuiLayer.h new file mode 100644 index 00000000..6134e63c --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanImGuiLayer.h @@ -0,0 +1,26 @@ +#pragma once + +#include "StarEngine/ImGui/ImGuiLayer.h" +#include "StarEngine/Renderer/RenderCommandBuffer.h" + +namespace StarEngine { + + class VulkanImGuiLayer : public ImGuiLayer + { + public: + VulkanImGuiLayer(); + VulkanImGuiLayer(const std::string& name); + virtual ~VulkanImGuiLayer(); + + void Begin(); + void End(); + + virtual void OnAttach() override; + virtual void OnDetach() override; + virtual void OnImGuiRender() override; + private: + Ref m_RenderCommandBuffer; + float m_Time = 0.0f; + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanMaterial.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanMaterial.cpp new file mode 100644 index 00000000..48c18b81 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanMaterial.cpp @@ -0,0 +1,361 @@ +#include "sepch.h" +#include "VulkanMaterial.h" + +#include "VulkanAPI.h" +#include "VulkanContext.h" +#include "VulkanRenderer.h" +#include "StarEngine/Renderer/Texture.h" +#include "StarEngine/Renderer/UniformBuffer.h" + +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Renderer/Renderer.h" + +#include + +namespace StarEngine { + + VulkanMaterial::VulkanMaterial(const Ref& shader, const std::string& name) + : m_Shader(shader.As()), m_Name(name) + { + Init(); + Renderer::RegisterShaderDependency(shader, this); + } + + VulkanMaterial::VulkanMaterial(Ref material, const std::string& name) + : m_Shader(material->GetShader().As()), m_Name(name) + { + if (name.empty()) + m_Name = material->GetName(); + + Init(); + Renderer::RegisterShaderDependency(m_Shader, this); + + auto vulkanMaterial = material.As(); + m_UniformStorageBuffer = Buffer::Copy(vulkanMaterial->m_UniformStorageBuffer.Data, vulkanMaterial->m_UniformStorageBuffer.Size); + m_DescriptorSetManager = DescriptorSetManager::Copy(vulkanMaterial->m_DescriptorSetManager); + } + + VulkanMaterial::~VulkanMaterial() + { + m_UniformStorageBuffer.Release(); + } + + void VulkanMaterial::Init() + { + AllocateStorage(); + + m_MaterialFlags |= (uint32_t)MaterialFlag::DepthTest; + m_MaterialFlags |= (uint32_t)MaterialFlag::Blend; + + DescriptorSetManagerSpecification dmSpec; + dmSpec.DebugName = m_Name.empty() ? std::format("{} (Material)", m_Shader->GetName()) : m_Name; + dmSpec.Shader = m_Shader.As(); + dmSpec.StartSet = 0; + dmSpec.EndSet = 0; + dmSpec.DefaultResources = true; + m_DescriptorSetManager = DescriptorSetManager(dmSpec); + + for (const auto& [name, decl] : m_DescriptorSetManager.InputDeclarations) + { + switch (decl.Type) + { + case RenderPassInputType::ImageSampler1D: + case RenderPassInputType::ImageSampler2D: + { + for (uint32_t i = 0; i < decl.Count; i++) + m_DescriptorSetManager.SetInput(name, Renderer::GetWhiteTexture(), i); + break; + } + case RenderPassInputType::ImageSampler3D: + { + m_DescriptorSetManager.SetInput(name, Renderer::GetBlackCubeTexture()); + break; + } + } + } + + SE_CORE_VERIFY(m_DescriptorSetManager.Validate()); + m_DescriptorSetManager.Bake(); + } + + void VulkanMaterial::Invalidate() + { + // Allocate descriptor set 0 based on shader layout + if (m_Shader->HasDescriptorSet(0)) + { + VkDescriptorSetLayout dsl = m_Shader->GetDescriptorSetLayout(0); + VkDescriptorSetAllocateInfo descriptorSetAllocInfo = Vulkan::DescriptorSetAllocInfo(&dsl); + uint32_t framesInFlight = Renderer::GetConfig().FramesInFlight; + m_MaterialDescriptorSets.resize(framesInFlight); + for (uint32_t i = 0; i < framesInFlight; i++) + m_MaterialDescriptorSets[i] = VulkanRenderer::AllocateMaterialDescriptorSet(descriptorSetAllocInfo); + + // Sort into map sorted by binding + const auto& shaderDescriptorSets = m_Shader->GetShaderDescriptorSets(); + std::map writeDescriptors; + std::set textureCubes; // temp + for (const auto& [name, writeDescriptor] : shaderDescriptorSets[0].WriteDescriptorSets) + { + if ( + writeDescriptor.descriptorType == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER || + writeDescriptor.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || + writeDescriptor.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) + { + writeDescriptors[writeDescriptor.dstBinding] = writeDescriptor; + + } + } + + // Ordered map + for (const auto& [binding, writeDescriptor] : writeDescriptors) + { + m_MaterialWriteDescriptors[binding] = writeDescriptor; + m_MaterialDescriptorImages[binding] = std::vector>(writeDescriptor.descriptorCount); + + if (writeDescriptor.descriptorType == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) + { + // Set default image infos + for (size_t i = 0; i < writeDescriptor.descriptorCount; i++) + { + // TODO: check if cube or 2D + m_MaterialDescriptorImages[binding][i] = Renderer::GetWhiteTexture(); + } + } + } + } + else + { + SE_CORE_WARN_TAG("Renderer", "[Material] - shader {} has no Set 0!", m_Shader->GetName()); + } + } + + void VulkanMaterial::AllocateStorage() + { + const auto& shaderBuffers = m_Shader->GetShaderBuffers(); + + if (shaderBuffers.size() > 0) + { + uint32_t size = 0; + for (auto [name, shaderBuffer] : shaderBuffers) + size += shaderBuffer.Size; + + m_UniformStorageBuffer.Allocate(size); + m_UniformStorageBuffer.ZeroInitialize(); + } + } + + void VulkanMaterial::OnShaderReloaded() + { + //Init(); + } + + const ShaderUniform* VulkanMaterial::FindUniformDeclaration(const std::string& name) + { + const auto& shaderBuffers = m_Shader->GetShaderBuffers(); + + SE_CORE_ASSERT(shaderBuffers.size() <= 1, "We currently only support ONE material buffer!"); + + if (shaderBuffers.size() > 0) + { + const ShaderBuffer& buffer = (*shaderBuffers.begin()).second; + if (buffer.Uniforms.find(name) == buffer.Uniforms.end()) + return nullptr; + + return &buffer.Uniforms.at(name); + } + return nullptr; + } + + const ShaderResourceDeclaration* VulkanMaterial::FindResourceDeclaration(const std::string& name) + { + auto& resources = m_Shader->GetResources(); + if (resources.find(name) != resources.end()) + return &resources.at(name); + + return nullptr; + } + + void VulkanMaterial::SetVulkanDescriptor(const std::string& name, const Ref& texture) + { + m_DescriptorSetManager.SetInput(name, texture); + } + + void VulkanMaterial::SetVulkanDescriptor(const std::string& name, const Ref& texture, uint32_t arrayIndex) + { + m_DescriptorSetManager.SetInput(name, texture, arrayIndex); + } + + void VulkanMaterial::SetVulkanDescriptor(const std::string& name, const Ref& texture) + { + m_DescriptorSetManager.SetInput(name, texture); + } + + void VulkanMaterial::SetVulkanDescriptor(const std::string& name, const Ref& image) + { + SE_CORE_VERIFY(image); + m_DescriptorSetManager.SetInput(name, image); + } + + void VulkanMaterial::SetVulkanDescriptor(const std::string& name, const Ref& image) + { + SE_CORE_VERIFY(image); + m_DescriptorSetManager.SetInput(name, image); + } + + void VulkanMaterial::Set(const std::string& name, float value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, int value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, uint32_t value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, bool value) + { + // Bools are 4-byte ints + Set(name, (int)value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::ivec2& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::ivec3& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::ivec4& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::vec2& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::vec3& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::vec4& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::mat3& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const glm::mat4& value) + { + Set(name, value); + } + + void VulkanMaterial::Set(const std::string& name, const Ref& texture) + { + SetVulkanDescriptor(name, texture); + } + + void VulkanMaterial::Set(const std::string& name, const Ref& texture, uint32_t arrayIndex) + { + SetVulkanDescriptor(name, texture, arrayIndex); + } + + void VulkanMaterial::Set(const std::string& name, const Ref& texture) + { + SetVulkanDescriptor(name, texture); + } + + void VulkanMaterial::Set(const std::string& name, const Ref& image) + { + SetVulkanDescriptor(name, image); + } + + void VulkanMaterial::Set(const std::string& name, const Ref& image) + { + SetVulkanDescriptor(name, image); + } + + float& VulkanMaterial::GetFloat(const std::string& name) + { + return Get(name); + } + + int32_t& VulkanMaterial::GetInt(const std::string& name) + { + return Get(name); + } + + uint32_t& VulkanMaterial::GetUInt(const std::string& name) + { + return Get(name); + } + + bool& VulkanMaterial::GetBool(const std::string& name) + { + return Get(name); + } + + glm::vec2& VulkanMaterial::GetVector2(const std::string& name) + { + return Get(name); + } + + glm::vec3& VulkanMaterial::GetVector3(const std::string& name) + { + return Get(name); + } + + glm::vec4& VulkanMaterial::GetVector4(const std::string& name) + { + return Get(name); + } + + glm::mat3& VulkanMaterial::GetMatrix3(const std::string& name) + { + return Get(name); + } + + glm::mat4& VulkanMaterial::GetMatrix4(const std::string& name) + { + return Get(name); + } + + Ref VulkanMaterial::GetTexture2D(const std::string& name) + { + return GetResource(name); + } + + Ref VulkanMaterial::TryGetTextureCube(const std::string& name) + { + return TryGetResource(name); + } + + Ref VulkanMaterial::TryGetTexture2D(const std::string& name) + { + return TryGetResource(name); + } + + Ref VulkanMaterial::GetTextureCube(const std::string& name) + { + return GetResource(name); + } + + void VulkanMaterial::Prepare() + { + m_DescriptorSetManager.InvalidateAndUpdate(); + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanMaterial.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanMaterial.h new file mode 100644 index 00000000..a98807b6 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanMaterial.h @@ -0,0 +1,156 @@ +#pragma once + +#include "StarEngine/Renderer/Material.h" + +#include "StarEngine/Platform/Vulkan/VulkanShader.h" +#include "StarEngine/Renderer/Image.h" +#include "StarEngine/Platform/Vulkan/DescriptorSetManager.h" + +namespace StarEngine { + + class VulkanMaterial : public Material + { + public: + VulkanMaterial(const Ref& shader, const std::string& name = ""); + VulkanMaterial(Ref material, const std::string& name = ""); + virtual ~VulkanMaterial() override; + + virtual void Invalidate() override; + virtual void OnShaderReloaded() override; + + virtual void Set(const std::string& name, float value) override; + virtual void Set(const std::string& name, int value) override; + virtual void Set(const std::string& name, uint32_t value) override; + virtual void Set(const std::string& name, bool value) override; + virtual void Set(const std::string& name, const glm::ivec2& value) override; + virtual void Set(const std::string& name, const glm::ivec3& value) override; + virtual void Set(const std::string& name, const glm::ivec4& value) override; + virtual void Set(const std::string& name, const glm::vec2& value) override; + virtual void Set(const std::string& name, const glm::vec3& value) override; + virtual void Set(const std::string& name, const glm::vec4& value) override; + virtual void Set(const std::string& name, const glm::mat3& value) override; + virtual void Set(const std::string& name, const glm::mat4& value) override; + + virtual void Set(const std::string& name, const Ref& texture) override; + virtual void Set(const std::string& name, const Ref& texture, uint32_t arrayIndex) override; + virtual void Set(const std::string& name, const Ref& texture) override; + virtual void Set(const std::string& name, const Ref& image) override; + virtual void Set(const std::string& name, const Ref& image) override; + + virtual float& GetFloat(const std::string& name) override; + virtual int32_t& GetInt(const std::string& name) override; + virtual uint32_t& GetUInt(const std::string& name) override; + virtual bool& GetBool(const std::string& name) override; + virtual glm::vec2& GetVector2(const std::string& name) override; + virtual glm::vec3& GetVector3(const std::string& name) override; + virtual glm::vec4& GetVector4(const std::string& name) override; + virtual glm::mat3& GetMatrix3(const std::string& name) override; + virtual glm::mat4& GetMatrix4(const std::string& name) override; + + virtual Ref GetTexture2D(const std::string& name) override; + virtual Ref GetTextureCube(const std::string& name) override; + + virtual Ref TryGetTexture2D(const std::string& name) override; + virtual Ref TryGetTextureCube(const std::string& name) override; + + template + void Set(const std::string& name, const T& value) + { + auto decl = FindUniformDeclaration(name); + SE_CORE_ASSERT(decl, "Could not find uniform!"); + if (!decl) + return; + + auto& buffer = m_UniformStorageBuffer; + buffer.Write((byte*)&value, decl->GetSize(), decl->GetOffset()); + } + + template + T& Get(const std::string& name) + { + auto decl = FindUniformDeclaration(name); + SE_CORE_ASSERT(decl, "Could not find uniform with name 'x'"); + auto& buffer = m_UniformStorageBuffer; + return buffer.Read(decl->GetOffset()); + } + + template + Ref GetResource(const std::string& name) + { + return m_DescriptorSetManager.GetInput(name); + } + + template + Ref TryGetResource(const std::string& name) + { + return m_DescriptorSetManager.GetInput(name); + } + + virtual uint32_t GetFlags() const override { return m_MaterialFlags; } + virtual void SetFlags(uint32_t flags) override { m_MaterialFlags = flags; } + virtual bool GetFlag(MaterialFlag flag) const override { return (uint32_t)flag & m_MaterialFlags; } + virtual void SetFlag(MaterialFlag flag, bool value = true) override + { + if (value) + { + m_MaterialFlags |= (uint32_t)flag; + } + else + { + m_MaterialFlags &= ~(uint32_t)flag; + } + } + + virtual Ref GetShader() override { return m_Shader; } + virtual const std::string& GetName() const override { return m_Name; } + + Buffer GetUniformStorageBuffer() { return m_UniformStorageBuffer; } + + VkDescriptorSet GetDescriptorSet(uint32_t index) + { + if (m_DescriptorSetManager.GetFirstSetIndex() == UINT32_MAX) + return nullptr; + + Prepare(); + return m_DescriptorSetManager.GetDescriptorSets(index)[0]; + } + + void Prepare(); + private: + void Init(); + void AllocateStorage(); + + void SetVulkanDescriptor(const std::string& name, const Ref& texture); + void SetVulkanDescriptor(const std::string& name, const Ref& texture, uint32_t arrayIndex); + void SetVulkanDescriptor(const std::string& name, const Ref& texture); + void SetVulkanDescriptor(const std::string& name, const Ref& image); + void SetVulkanDescriptor(const std::string& name, const Ref& image); + + const ShaderUniform* FindUniformDeclaration(const std::string& name); + const ShaderResourceDeclaration* FindResourceDeclaration(const std::string& name); + private: + Ref m_Shader; + std::string m_Name; + + enum class PendingDescriptorType + { + None = 0, Texture2D, TextureCube, Image2D + }; + + // -- NEW v -- + // Per frame in flight + DescriptorSetManager m_DescriptorSetManager; + std::vector m_MaterialDescriptorSets; + + // Map key is binding, vector index is array index (size 1 for non-array) + std::map>> m_MaterialDescriptorImages; + std::map m_MaterialWriteDescriptors; + // -- NEW ^ -- + + uint32_t m_MaterialFlags = 0; + + Buffer m_UniformStorageBuffer; + + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderCommandBuffer.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderCommandBuffer.cpp new file mode 100644 index 00000000..c0108d81 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderCommandBuffer.cpp @@ -0,0 +1,282 @@ +#include "sepch.h" +#include "VulkanRenderCommandBuffer.h" + +#include "VulkanContext.h" + +#include +#include + +namespace StarEngine { + + VulkanRenderCommandBuffer::VulkanRenderCommandBuffer(uint32_t commandBufferCount, std::string debugName) + : m_DebugName(std::move(debugName)) + { + auto device = VulkanContext::GetCurrentDevice(); + + if (commandBufferCount == 0) + { + // 0 means one per frame in flight + commandBufferCount = Renderer::GetConfig().FramesInFlight; + } + + SE_CORE_VERIFY(commandBufferCount > 0); + + VkCommandPoolCreateInfo cmdPoolInfo = {}; + cmdPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + cmdPoolInfo.queueFamilyIndex = device->GetPhysicalDevice()->GetQueueFamilyIndices().Graphics; + cmdPoolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + VK_CHECK_RESULT(vkCreateCommandPool(device->GetVulkanDevice(), &cmdPoolInfo, nullptr, &m_CommandPool)); + VKUtils::SetDebugUtilsObjectName(device->GetVulkanDevice(), VK_OBJECT_TYPE_COMMAND_POOL, m_DebugName, m_CommandPool); + + VkCommandBufferAllocateInfo commandBufferAllocateInfo{}; + commandBufferAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + commandBufferAllocateInfo.commandPool = m_CommandPool; + commandBufferAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + commandBufferAllocateInfo.commandBufferCount = commandBufferCount; + m_CommandBuffers.resize(commandBufferCount); + VK_CHECK_RESULT(vkAllocateCommandBuffers(device->GetVulkanDevice(), &commandBufferAllocateInfo, m_CommandBuffers.data())); + + for (uint32_t i = 0; i < commandBufferCount; ++i) + VKUtils::SetDebugUtilsObjectName(device->GetVulkanDevice(), VK_OBJECT_TYPE_COMMAND_BUFFER, std::format("{} (frame in flight: {})", m_DebugName, i), m_CommandBuffers[i]); + + VkFenceCreateInfo fenceCreateInfo{}; + fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + m_WaitFences.resize(commandBufferCount); + for (size_t i = 0; i < m_WaitFences.size(); ++i) + { + VK_CHECK_RESULT(vkCreateFence(device->GetVulkanDevice(), &fenceCreateInfo, nullptr, &m_WaitFences[i])); + VKUtils::SetDebugUtilsObjectName(device->GetVulkanDevice(), VK_OBJECT_TYPE_FENCE, std::format("{} (frame in flight: {}) fence", m_DebugName, i), m_WaitFences[i]); + } + + VkQueryPoolCreateInfo queryPoolCreateInfo = {}; + queryPoolCreateInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + queryPoolCreateInfo.pNext = nullptr; + + // Timestamp queries + const uint32_t maxUserQueries = 16; + m_TimestampQueryCount = 2 + 2 * maxUserQueries; + + queryPoolCreateInfo.queryType = VK_QUERY_TYPE_TIMESTAMP; + queryPoolCreateInfo.queryCount = m_TimestampQueryCount; + m_TimestampQueryPools.resize(commandBufferCount); + for (auto& timestampQueryPool : m_TimestampQueryPools) + VK_CHECK_RESULT(vkCreateQueryPool(device->GetVulkanDevice(), &queryPoolCreateInfo, nullptr, ×tampQueryPool)); + + m_TimestampQueryResults.resize(commandBufferCount); + for (auto& timestampQueryResults : m_TimestampQueryResults) + timestampQueryResults.resize(m_TimestampQueryCount); + + m_ExecutionGPUTimes.resize(commandBufferCount); + for (auto& executionGPUTimes : m_ExecutionGPUTimes) + executionGPUTimes.resize(m_TimestampQueryCount / 2); + + // Pipeline statistics queries + m_PipelineQueryCount = 7; + queryPoolCreateInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS; + queryPoolCreateInfo.queryCount = m_PipelineQueryCount; + queryPoolCreateInfo.pipelineStatistics = + VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_VERTICES_BIT | + VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_PRIMITIVES_BIT | + VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_CLIPPING_PRIMITIVES_BIT | + VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT; + + m_PipelineStatisticsQueryPools.resize(commandBufferCount); + for (auto& pipelineStatisticsQueryPools : m_PipelineStatisticsQueryPools) + VK_CHECK_RESULT(vkCreateQueryPool(device->GetVulkanDevice(), &queryPoolCreateInfo, nullptr, &pipelineStatisticsQueryPools)); + + m_PipelineStatisticsQueryResults.resize(commandBufferCount); + } + + VulkanRenderCommandBuffer::VulkanRenderCommandBuffer(std::string debugName, bool swapchain) + : m_DebugName(std::move(debugName)), m_OwnedBySwapChain(true) + { + auto device = VulkanContext::GetCurrentDevice(); + uint32_t framesInFlight = Renderer::GetConfig().FramesInFlight; + + VkQueryPoolCreateInfo queryPoolCreateInfo = {}; + queryPoolCreateInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + queryPoolCreateInfo.pNext = nullptr; + + // Timestamp queries + const uint32_t maxUserQueries = 16; + m_TimestampQueryCount = 2 + 2 * maxUserQueries; + + queryPoolCreateInfo.queryType = VK_QUERY_TYPE_TIMESTAMP; + queryPoolCreateInfo.queryCount = m_TimestampQueryCount; + m_TimestampQueryPools.resize(framesInFlight); + for (auto& timestampQueryPool : m_TimestampQueryPools) + VK_CHECK_RESULT(vkCreateQueryPool(device->GetVulkanDevice(), &queryPoolCreateInfo, nullptr, ×tampQueryPool)); + + m_TimestampQueryResults.resize(framesInFlight); + for (auto& timestampQueryResults : m_TimestampQueryResults) + timestampQueryResults.resize(m_TimestampQueryCount); + + m_ExecutionGPUTimes.resize(framesInFlight); + for (auto& executionGPUTimes : m_ExecutionGPUTimes) + executionGPUTimes.resize(m_TimestampQueryCount / 2); + + // Pipeline statistics queries + m_PipelineQueryCount = 7; + queryPoolCreateInfo.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS; + queryPoolCreateInfo.queryCount = m_PipelineQueryCount; + queryPoolCreateInfo.pipelineStatistics = + VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_VERTICES_BIT | + VK_QUERY_PIPELINE_STATISTIC_INPUT_ASSEMBLY_PRIMITIVES_BIT | + VK_QUERY_PIPELINE_STATISTIC_VERTEX_SHADER_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_CLIPPING_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_CLIPPING_PRIMITIVES_BIT | + VK_QUERY_PIPELINE_STATISTIC_FRAGMENT_SHADER_INVOCATIONS_BIT | + VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT; + + m_PipelineStatisticsQueryPools.resize(framesInFlight); + for (auto& pipelineStatisticsQueryPools : m_PipelineStatisticsQueryPools) + VK_CHECK_RESULT(vkCreateQueryPool(device->GetVulkanDevice(), &queryPoolCreateInfo, nullptr, &pipelineStatisticsQueryPools)); + + m_PipelineStatisticsQueryResults.resize(framesInFlight); + } + + VulkanRenderCommandBuffer::~VulkanRenderCommandBuffer() + { + if (m_OwnedBySwapChain) + return; + + VkCommandPool commandPool = m_CommandPool; + Renderer::SubmitResourceFree([commandPool]() + { + auto device = VulkanContext::GetCurrentDevice(); + vkDestroyCommandPool(device->GetVulkanDevice(), commandPool, nullptr); + }); + } + + void VulkanRenderCommandBuffer::Begin() + { + m_TimestampNextAvailableQuery = 2; + + + Ref instance = this; + Renderer::Submit([instance]() mutable + { + uint32_t commandBufferIndex = Renderer::RT_GetCurrentFrameIndex(); + + VkCommandBufferBeginInfo cmdBufInfo = {}; + cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + cmdBufInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + cmdBufInfo.pNext = nullptr; + + VkCommandBuffer commandBuffer = nullptr; + if (instance->m_OwnedBySwapChain) + { + VulkanSwapChain& swapChain = Application::Get().GetWindow().GetSwapChain(); + // TODO(Yan): commandBuffer = swapChain.GetDrawCommandBuffer(commandBufferIndex); + } + else + { + commandBufferIndex %= instance->m_CommandBuffers.size(); + commandBuffer = instance->m_CommandBuffers[commandBufferIndex]; + } + instance->m_ActiveCommandBuffer = commandBuffer; + VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffer, &cmdBufInfo)); + + // Timestamp query + vkCmdResetQueryPool(commandBuffer, instance->m_TimestampQueryPools[commandBufferIndex], 0, instance->m_TimestampQueryCount); + vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, instance->m_TimestampQueryPools[commandBufferIndex], 0); + + // Pipeline stats query + vkCmdResetQueryPool(commandBuffer, instance->m_PipelineStatisticsQueryPools[commandBufferIndex], 0, instance->m_PipelineQueryCount); + vkCmdBeginQuery(commandBuffer, instance->m_PipelineStatisticsQueryPools[commandBufferIndex], 0, 0); + }); + } + + void VulkanRenderCommandBuffer::End() + { + Ref instance = this; + Renderer::Submit([instance]() mutable + { + uint32_t commandBufferIndex = Renderer::RT_GetCurrentFrameIndex(); + if (!instance->m_OwnedBySwapChain) + commandBufferIndex %= instance->m_CommandBuffers.size(); + + VkCommandBuffer commandBuffer = instance->m_ActiveCommandBuffer; + vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, instance->m_TimestampQueryPools[commandBufferIndex], 1); + vkCmdEndQuery(commandBuffer, instance->m_PipelineStatisticsQueryPools[commandBufferIndex], 0); + VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer)); + + instance->m_ActiveCommandBuffer = nullptr; + }); + } + + void VulkanRenderCommandBuffer::Submit() + { + if (m_OwnedBySwapChain) + return; + + Ref instance = this; + Renderer::Submit([instance]() mutable + { + auto device = VulkanContext::GetCurrentDevice(); + + uint32_t commandBufferIndex = Renderer::RT_GetCurrentFrameIndex() % instance->m_CommandBuffers.size(); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + VkCommandBuffer commandBuffer = instance->m_CommandBuffers[commandBufferIndex]; + submitInfo.pCommandBuffers = &commandBuffer; + + VK_CHECK_RESULT(vkWaitForFences(device->GetVulkanDevice(), 1, &instance->m_WaitFences[commandBufferIndex], VK_TRUE, UINT64_MAX)); + VK_CHECK_RESULT(vkResetFences(device->GetVulkanDevice(), 1, &instance->m_WaitFences[commandBufferIndex])); + + SE_CORE_TRACE_TAG("Renderer", "Submitting Render Command Buffer {}", instance->m_DebugName); + + device->LockQueue(); + VK_CHECK_RESULT(vkQueueSubmit(device->GetGraphicsQueue(), 1, &submitInfo, instance->m_WaitFences[commandBufferIndex])); + device->UnlockQueue(); + + // Retrieve timestamp query results + vkGetQueryPoolResults(device->GetVulkanDevice(), instance->m_TimestampQueryPools[commandBufferIndex], 0, instance->m_TimestampNextAvailableQuery, + instance->m_TimestampNextAvailableQuery * sizeof(uint64_t), instance->m_TimestampQueryResults[commandBufferIndex].data(), sizeof(uint64_t), VK_QUERY_RESULT_64_BIT); + + for (uint32_t i = 0; i < instance->m_TimestampNextAvailableQuery; i += 2) + { + uint64_t startTime = instance->m_TimestampQueryResults[commandBufferIndex][i]; + uint64_t endTime = instance->m_TimestampQueryResults[commandBufferIndex][i + 1]; + float nsTime = endTime > startTime ? (endTime - startTime) * device->GetPhysicalDevice()->GetLimits().timestampPeriod : 0.0f; + instance->m_ExecutionGPUTimes[commandBufferIndex][i / 2] = nsTime * 0.000001f; // Time in ms + } + + // Retrieve pipeline stats results + vkGetQueryPoolResults(device->GetVulkanDevice(), instance->m_PipelineStatisticsQueryPools[commandBufferIndex], 0, 1, + sizeof(PipelineStatistics), &instance->m_PipelineStatisticsQueryResults[commandBufferIndex], sizeof(uint64_t), VK_QUERY_RESULT_64_BIT); + }); + } + + uint32_t VulkanRenderCommandBuffer::BeginTimestampQuery() + { + uint32_t queryIndex = m_TimestampNextAvailableQuery; + m_TimestampNextAvailableQuery += 2; + Ref instance = this; + Renderer::Submit([instance, queryIndex]() + { + uint32_t commandBufferIndex = Renderer::RT_GetCurrentFrameIndex() % instance->m_CommandBuffers.size(); + VkCommandBuffer commandBuffer = instance->m_CommandBuffers[commandBufferIndex]; + vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, instance->m_TimestampQueryPools[commandBufferIndex], queryIndex); + }); + return queryIndex; + } + + void VulkanRenderCommandBuffer::EndTimestampQuery(uint32_t queryID) + { + Ref instance = this; + Renderer::Submit([instance, queryID]() + { + uint32_t commandBufferIndex = Renderer::RT_GetCurrentFrameIndex() % instance->m_CommandBuffers.size(); + VkCommandBuffer commandBuffer = instance->m_CommandBuffers[commandBufferIndex]; + vkCmdWriteTimestamp(commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, instance->m_TimestampQueryPools[commandBufferIndex], queryID + 1); + }); + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderCommandBuffer.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderCommandBuffer.h new file mode 100644 index 00000000..7dd830b3 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderCommandBuffer.h @@ -0,0 +1,59 @@ +#pragma once + +#include "StarEngine/Renderer/RenderCommandBuffer.h" +#include "vulkan/vulkan.h" + +namespace StarEngine { + + class VulkanRenderCommandBuffer : public RefCounted + { + public: + VulkanRenderCommandBuffer(uint32_t count = 0, std::string debugName = ""); + VulkanRenderCommandBuffer(std::string debugName, bool swapchain); + ~VulkanRenderCommandBuffer(); + + void Begin(); + void End(); + void Submit(); + + float GetExecutionGPUTime(uint32_t frameIndex, uint32_t queryIndex = 0) const + { + if (queryIndex == UINT32_MAX || queryIndex / 2 >= m_TimestampNextAvailableQuery / 2) + return 0.0f; + + return m_ExecutionGPUTimes[frameIndex][queryIndex / 2]; + } + + const PipelineStatistics& GetPipelineStatistics(uint32_t frameIndex) const { return m_PipelineStatisticsQueryResults[frameIndex]; } + + uint32_t BeginTimestampQuery(); + void EndTimestampQuery(uint32_t queryID); + + VkCommandBuffer GetActiveCommandBuffer() const { return m_ActiveCommandBuffer; } + + VkCommandBuffer GetCommandBuffer(uint32_t frameIndex) const + { + SE_CORE_ASSERT(frameIndex < m_CommandBuffers.size()); + return m_CommandBuffers[frameIndex]; + } + private: + std::string m_DebugName; + VkCommandPool m_CommandPool = nullptr; + std::vector m_CommandBuffers; + VkCommandBuffer m_ActiveCommandBuffer = nullptr; + std::vector m_WaitFences; + + bool m_OwnedBySwapChain = false; + + uint32_t m_TimestampQueryCount = 0; + uint32_t m_TimestampNextAvailableQuery = 2; + std::vector m_TimestampQueryPools; + std::vector m_PipelineStatisticsQueryPools; + std::vector> m_TimestampQueryResults; + std::vector> m_ExecutionGPUTimes; + + uint32_t m_PipelineQueryCount = 0; + std::vector m_PipelineStatisticsQueryResults; + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderer.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderer.cpp new file mode 100644 index 00000000..57939ebc --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderer.cpp @@ -0,0 +1,1873 @@ +#include "sepch.h" +#if TODO +#include "VulkanRenderer.h" + +#include "Vulkan.h" +#include "VulkanAPI.h" +#include "VulkanComputePass.h" +#include "VulkanContext.h" +#include "VulkanFramebuffer.h" +#include "VulkanIndexBuffer.h" +#include "VulkanPipeline.h" +#include "VulkanRenderCommandBuffer.h" +#include "VulkanRenderPass.h" +#include "VulkanShader.h" +#include "VulkanTexture.h" +#include "VulkanVertexBuffer.h" + +#if SE_HAS_SHADER_COMPILER +#include "ShaderCompiler/VulkanShaderCompiler.h" +#endif + +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Core/Timer.h" +#include "StarEngine/Debug/Profiler.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Renderer/SceneRenderer.h" + +#include +#include +#include +#include +#include + +#include + +namespace StarEngine { + struct VulkanRendererData + { + RendererCapabilities RenderCaps; + + Ref BRDFLut; + + Ref QuadVertexBuffer; + Ref QuadIndexBuffer; + VulkanShader::ShaderMaterialDescriptorSet QuadDescriptorSet; + + std::unordered_map> RendererDescriptorSet; + VkDescriptorSet ActiveRendererDescriptorSet = nullptr; + std::vector DescriptorPools; + VkDescriptorPool MaterialDescriptorPool; + std::vector DescriptorPoolAllocationCount; + + // UniformBufferSet -> Shader Hash -> Frame -> WriteDescriptor + std::unordered_map>>> UniformBufferWriteDescriptorCache; + std::unordered_map>>> StorageBufferWriteDescriptorCache; + + // Default samplers + VkSampler SamplerClamp = nullptr; + VkSampler SamplerPoint = nullptr; + + int32_t SelectedDrawCall = -1; + int32_t DrawCallCount = 0; + }; + + static VulkanRendererData* s_Data = nullptr; + + namespace Utils { + + static const char* VulkanVendorIDToString(uint32_t vendorID) + { + switch (vendorID) + { + case 0x10DE: return "NVIDIA"; + case 0x1002: return "AMD"; + case 0x8086: return "INTEL"; + case 0x13B5: return "ARM"; + } + return "Unknown"; + } + + } + + void VulkanRenderer::Init() + { + s_Data = snew VulkanRendererData(); + const auto& config = Renderer::GetConfig(); + s_Data->DescriptorPools.resize(config.FramesInFlight); + s_Data->DescriptorPoolAllocationCount.resize(config.FramesInFlight); + + auto& caps = s_Data->RenderCaps; + auto& properties = VulkanContext::GetCurrentDevice()->GetPhysicalDevice()->GetProperties(); + caps.Vendor = Utils::VulkanVendorIDToString(properties.vendorID); + caps.Device = properties.deviceName; + caps.Version = std::to_string(properties.driverVersion); + + Utils::DumpGPUInfo(); + + // Create descriptor pools + Renderer::Submit([]() mutable + { + // Create Descriptor Pool + VkDescriptorPoolSize pool_sizes[] = + { + { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 }, + { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }, + { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 }, + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 }, + { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 } + }; + VkDescriptorPoolCreateInfo pool_info = {}; + pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + pool_info.maxSets = 100000; + pool_info.poolSizeCount = (uint32_t)IM_ARRAYSIZE(pool_sizes); + pool_info.pPoolSizes = pool_sizes; + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + uint32_t framesInFlight = Renderer::GetConfig().FramesInFlight; + for (uint32_t i = 0; i < framesInFlight; i++) + { + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &pool_info, nullptr, &s_Data->DescriptorPools[i])); + s_Data->DescriptorPoolAllocationCount[i] = 0; + } + + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &pool_info, nullptr, &s_Data->MaterialDescriptorPool)); + }); + + // Create fullscreen quad + float x = -1; + float y = -1; + float width = 2, height = 2; + struct QuadVertex + { + glm::vec3 Position; + glm::vec2 TexCoord; + }; + + QuadVertex* data = snew QuadVertex[4]; + + data[0].Position = glm::vec3(x, y, 0.0f); + data[0].TexCoord = glm::vec2(0, 0); + + data[1].Position = glm::vec3(x + width, y, 0.0f); + data[1].TexCoord = glm::vec2(1, 0); + + data[2].Position = glm::vec3(x + width, y + height, 0.0f); + data[2].TexCoord = glm::vec2(1, 1); + + data[3].Position = glm::vec3(x, y + height, 0.0f); + data[3].TexCoord = glm::vec2(0, 1); + + s_Data->QuadVertexBuffer = VertexBuffer::Create(data, 4 * sizeof(QuadVertex)); + uint32_t indices[6] = { 0, 1, 2, 2, 3, 0, }; + s_Data->QuadIndexBuffer = IndexBuffer::Create(indices, 6 * sizeof(uint32_t)); + + s_Data->BRDFLut = Renderer::GetBRDFLutTexture(); + } + + void VulkanRenderer::Shutdown() + { + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + vkDeviceWaitIdle(device); + + if (s_Data->SamplerPoint) + { + Vulkan::DestroySampler(s_Data->SamplerPoint); + s_Data->SamplerPoint = nullptr; + } + + if (s_Data->SamplerClamp) + { + Vulkan::DestroySampler(s_Data->SamplerClamp); + s_Data->SamplerClamp = nullptr; + } + +#if SE_HAS_SHADER_COMPILER + VulkanShaderCompiler::ClearUniformBuffers(); +#endif + delete s_Data; + } + + RendererCapabilities& VulkanRenderer::GetCapabilities() + { + return s_Data->RenderCaps; + } + + VkSampler VulkanRenderer::GetClampSampler() + { + if (s_Data->SamplerClamp) + return s_Data->SamplerClamp; + + VkSamplerCreateInfo samplerCreateInfo = {}; + samplerCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerCreateInfo.maxAnisotropy = 1.0f; + samplerCreateInfo.magFilter = VK_FILTER_LINEAR; + samplerCreateInfo.minFilter = VK_FILTER_LINEAR; + samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerCreateInfo.addressModeV = samplerCreateInfo.addressModeU; + samplerCreateInfo.addressModeW = samplerCreateInfo.addressModeU; + samplerCreateInfo.mipLodBias = 0.0f; + samplerCreateInfo.maxAnisotropy = 1.0f; + samplerCreateInfo.minLod = 0.0f; + samplerCreateInfo.maxLod = 100.0f; + samplerCreateInfo.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + s_Data->SamplerClamp = Vulkan::CreateSampler(samplerCreateInfo); + return s_Data->SamplerClamp; + } + + VkSampler VulkanRenderer::GetPointSampler() + { + if (s_Data->SamplerPoint) + return s_Data->SamplerPoint; + + VkSamplerCreateInfo samplerCreateInfo = {}; + samplerCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerCreateInfo.maxAnisotropy = 1.0f; + samplerCreateInfo.magFilter = VK_FILTER_NEAREST; + samplerCreateInfo.minFilter = VK_FILTER_NEAREST; + samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerCreateInfo.addressModeV = samplerCreateInfo.addressModeU; + samplerCreateInfo.addressModeW = samplerCreateInfo.addressModeU; + samplerCreateInfo.mipLodBias = 0.0f; + samplerCreateInfo.maxAnisotropy = 1.0f; + samplerCreateInfo.minLod = 0.0f; + samplerCreateInfo.maxLod = 100.0f; + samplerCreateInfo.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + s_Data->SamplerPoint = Vulkan::CreateSampler(samplerCreateInfo); + return s_Data->SamplerPoint; + } + + int32_t& VulkanRenderer::GetSelectedDrawCall() + { + return s_Data->SelectedDrawCall; + } + + void VulkanRenderer::RenderStaticMesh(Ref renderCommandBuffer, Ref pipeline, Ref mesh, Ref meshSource, uint32_t submeshIndex, Ref materialTable, Ref transformBuffer, uint32_t transformOffset, uint32_t instanceCount) + { + SE_CORE_VERIFY(mesh); + SE_CORE_VERIFY(meshSource); + SE_CORE_VERIFY(materialTable); + + Renderer::Submit([renderCommandBuffer, pipeline, mesh, meshSource, submeshIndex, materialTable = Ref::Create(materialTable), transformBuffer, transformOffset, instanceCount]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::RenderMesh"); + SE_SCOPE_PERF("VulkanRenderer::RenderMesh"); + + if (s_Data->SelectedDrawCall != -1 && s_Data->DrawCallCount > s_Data->SelectedDrawCall) + return; + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + Ref vulkanMeshVB = meshSource->GetVertexBuffer().As(); + VkBuffer vbMeshBuffer = vulkanMeshVB->GetVulkanBuffer(); + VkDeviceSize offsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vbMeshBuffer, offsets); + + Ref vulkanTransformBuffer = transformBuffer.As(); + VkBuffer vbTransformBuffer = vulkanTransformBuffer->GetVulkanBuffer(); + VkDeviceSize instanceOffsets[1] = { transformOffset }; + vkCmdBindVertexBuffers(commandBuffer, 1, 1, &vbTransformBuffer, instanceOffsets); + + auto vulkanMeshIB = Ref(meshSource->GetIndexBuffer()); + VkBuffer ibBuffer = vulkanMeshIB->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, ibBuffer, 0, VK_INDEX_TYPE_UINT32); + + Ref vulkanPipeline = pipeline.As(); + + std::vector> writeDescriptors; + + const auto& submeshes = meshSource->GetSubmeshes(); + const Submesh& submesh = submeshes[submeshIndex]; + Ref meshMaterialTable = mesh->GetMaterials(); + uint32_t materialCount = meshMaterialTable->GetMaterialCount(); + + // NOTE(Yan): probably should not involve Asset Manager at this stage + AssetHandle materialHandle = materialTable->HasMaterial(submesh.MaterialIndex) ? materialTable->GetMaterial(submesh.MaterialIndex) : meshMaterialTable->GetMaterial(submesh.MaterialIndex); + Ref material = AssetManager::GetAsset(materialHandle); + Ref vulkanMaterial = material->GetMaterial().As(); + + if (s_Data->SelectedDrawCall != -1 && s_Data->DrawCallCount > s_Data->SelectedDrawCall) + return; + + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descriptorSet, 0, nullptr); + + Buffer uniformStorageBuffer = vulkanMaterial->GetUniformStorageBuffer(); + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, (uint32_t)uniformStorageBuffer.Size, uniformStorageBuffer.Data); + + vkCmdDrawIndexed(commandBuffer, submesh.IndexCount, instanceCount, submesh.BaseIndex, submesh.BaseVertex, 0); + s_Data->DrawCallCount++; + }); + } + + void VulkanRenderer::RenderSubmeshInstanced(Ref renderCommandBuffer, Ref pipeline, Ref mesh, Ref meshSource, uint32_t submeshIndex, Ref materialTable, Ref transformBuffer, uint32_t transformOffset, uint32_t boneTransformsOffset, uint32_t boneTransformsStride, uint32_t instanceCount) + { + SE_CORE_VERIFY(mesh); + SE_CORE_VERIFY(meshSource); + SE_CORE_VERIFY(materialTable); + + Renderer::Submit([renderCommandBuffer, pipeline, mesh, meshSource, submeshIndex, materialTable, transformBuffer, transformOffset, boneTransformsOffset, boneTransformsStride, instanceCount]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::RenderMesh"); + SE_SCOPE_PERF("VulkanRenderer::RenderMesh"); + + if (s_Data->SelectedDrawCall != -1 && s_Data->DrawCallCount > s_Data->SelectedDrawCall) + return; + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + Ref vulkanMeshVB = meshSource->GetVertexBuffer().As(); + VkBuffer vbMeshBuffer = vulkanMeshVB->GetVulkanBuffer(); + VkDeviceSize vertexOffsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vbMeshBuffer, vertexOffsets); + + Ref vulkanTransformBuffer = transformBuffer.As(); + VkBuffer vbTransformBuffer = vulkanTransformBuffer->GetVulkanBuffer(); + VkDeviceSize instanceOffsets[1] = { transformOffset }; + vkCmdBindVertexBuffers(commandBuffer, 1, 1, &vbTransformBuffer, instanceOffsets); + + auto vulkanMeshIB = Ref(meshSource->GetIndexBuffer()); + VkBuffer ibBuffer = vulkanMeshIB->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, ibBuffer, 0, VK_INDEX_TYPE_UINT32); + + Ref vulkanPipeline = pipeline.As(); + + const auto& submeshes = meshSource->GetSubmeshes(); + const auto& submesh = submeshes[submeshIndex]; + + if (submesh.IsRigged) + { + VkBuffer boneInfluenceVB = meshSource->GetBoneInfluenceBuffer().As()->GetVulkanBuffer(); + vkCmdBindVertexBuffers(commandBuffer, 2, 1, &boneInfluenceVB, vertexOffsets); + } + + Ref meshMaterialTable = mesh->GetMaterials(); + uint32_t materialCount = meshMaterialTable->GetMaterialCount(); + // NOTE(Yan): probably should not involve Asset Manager at this stage + AssetHandle materialHandle = materialTable->HasMaterial(submesh.MaterialIndex) ? materialTable->GetMaterial(submesh.MaterialIndex) : meshMaterialTable->GetMaterial(submesh.MaterialIndex); + Ref material = AssetManager::GetAsset(materialHandle); //RT_UpdateMaterialForRendering(vulkanMaterial, uniformBufferSet, storageBufferSet); + + if (s_Data->SelectedDrawCall != -1 && s_Data->DrawCallCount > s_Data->SelectedDrawCall) + return; + + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + Ref vulkanMaterial = material->GetMaterial().As(); + uint32_t pushConstantOffset = 0; + if (submesh.IsRigged) + { + uint32_t boneTransformsOffsetStride[2] = { boneTransformsOffset, boneTransformsStride }; + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_VERTEX_BIT, pushConstantOffset, sizeof(uint32_t) * 2, boneTransformsOffsetStride); + pushConstantOffset += 16; // TODO: it's 16 because that happens to be the offset that is declared for the material push constants in the shaders. Need a better way of doing this. Cannot just use the size of the pushConstantBuffer, because you dont know what alignment the next push constant range might have + } + + if (vulkanMaterial) + { + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descriptorSet, 0, nullptr); + + Buffer uniformStorageBuffer = vulkanMaterial->GetUniformStorageBuffer(); + if (uniformStorageBuffer) + { + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, pushConstantOffset, uniformStorageBuffer.Size, uniformStorageBuffer.Data); + } + } + + vkCmdDrawIndexed(commandBuffer, submesh.IndexCount, instanceCount, submesh.BaseIndex, submesh.BaseVertex, 0); + s_Data->DrawCallCount++; + }); + } + + void VulkanRenderer::RenderMeshWithMaterial(Ref renderCommandBuffer, Ref pipeline, Ref mesh, Ref meshSource, uint32_t submeshIndex, Ref material, Ref transformBuffer, uint32_t transformOffset, uint32_t boneTransformsOffset, uint32_t boneTransformsStride, uint32_t instanceCount, Buffer additionalUniforms) + { + SE_CORE_VERIFY(mesh); + SE_CORE_VERIFY(meshSource); + SE_CORE_VERIFY(material); + + Buffer pushConstantBuffer; + bool isRigged = meshSource->IsSubmeshRigged(submeshIndex); + + if (additionalUniforms.Size || isRigged) + { + pushConstantBuffer.Allocate(additionalUniforms.Size + (isRigged ? sizeof(uint32_t) * 2 : 0)); + if (additionalUniforms.Size) + pushConstantBuffer.Write(additionalUniforms.Data, additionalUniforms.Size); + + if (isRigged) + { + uint32_t boneTransformsOffsetStride[2] = { boneTransformsOffset, boneTransformsStride }; + pushConstantBuffer.Write(boneTransformsOffsetStride, sizeof(uint32_t) * 2, additionalUniforms.Size); + } + } + + Ref vulkanMaterial = material.As(); + Renderer::Submit([renderCommandBuffer, pipeline, mesh, meshSource, submeshIndex, vulkanMaterial, transformBuffer, transformOffset, instanceCount, pushConstantBuffer]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::RenderMeshWithMaterial"); + SE_SCOPE_PERF("VulkanRenderer::RenderMeshWithMaterial"); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + VkBuffer meshVB = meshSource->GetVertexBuffer().As()->GetVulkanBuffer(); + VkDeviceSize vertexOffsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &meshVB, vertexOffsets); + + VkBuffer transformVB = transformBuffer.As()->GetVulkanBuffer(); + VkDeviceSize instanceOffsets[1] = { transformOffset }; + vkCmdBindVertexBuffers(commandBuffer, 1, 1, &transformVB, instanceOffsets); + + VkBuffer meshIB = meshSource->GetIndexBuffer().As()->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, meshIB, 0, VK_INDEX_TYPE_UINT32); + + //RT_UpdateMaterialForRendering(vulkanMaterial, uniformBufferSet, storageBufferSet); + + Ref vulkanPipeline = pipeline.As(); + + const auto& submeshes = meshSource->GetSubmeshes(); + const auto& submesh = submeshes[submeshIndex]; + + if (submesh.IsRigged) + { + Ref vulkanBoneInfluencesVB = meshSource->GetBoneInfluenceBuffer().As(); + VkBuffer vbBoneInfluencesBuffer = vulkanBoneInfluencesVB->GetVulkanBuffer(); + vkCmdBindVertexBuffers(commandBuffer, 2, 1, &vbBoneInfluencesBuffer, vertexOffsets); + } + + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + + uint32_t pushConstantOffset = 0; + if (pushConstantBuffer.Size) + { + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_VERTEX_BIT, pushConstantOffset, pushConstantBuffer.Size, pushConstantBuffer.Data); + pushConstantOffset += 16; // TODO: it's 16 because that happens to be the offset that is declared for the material push constants in the shaders. Need a better way of doing this. Cannot just use the size of the pushConstantBuffer, because you dont know what alignment the next push constant range might have + } + + // Bind descriptor sets describing shader binding points + // NOTE: Descriptor Set 0 is the material, Descriptor Set 1 (if present) is the animation data + // std::vector descriptorSets; + if (vulkanMaterial) + { + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descriptorSet, 0, nullptr); + + Buffer uniformStorageBuffer = vulkanMaterial->GetUniformStorageBuffer(); + if (uniformStorageBuffer.Size) + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, pushConstantOffset, uniformStorageBuffer.Size, uniformStorageBuffer.Data); + } + + vkCmdDrawIndexed(commandBuffer, submesh.IndexCount, instanceCount, submesh.BaseIndex, submesh.BaseVertex, 0); + + pushConstantBuffer.Release(); + }); + } + + void VulkanRenderer::RenderStaticMeshWithMaterial(Ref renderCommandBuffer, Ref pipeline, Ref staticMesh, Ref meshSource, uint32_t submeshIndex, Ref material, Ref transformBuffer, uint32_t transformOffset, uint32_t instanceCount, Buffer additionalUniforms /*= Buffer()*/) + { + SE_CORE_ASSERT(staticMesh); + SE_CORE_ASSERT(meshSource); + SE_CORE_ASSERT(material); + + Buffer pushConstantBuffer; + if (additionalUniforms.Size) + { + pushConstantBuffer.Allocate(additionalUniforms.Size); + if (additionalUniforms.Size) + pushConstantBuffer.Write(additionalUniforms.Data, additionalUniforms.Size); + } + + Ref vulkanMaterial = material.As(); + Renderer::Submit([renderCommandBuffer, pipeline, staticMesh, meshSource, submeshIndex, vulkanMaterial, transformBuffer, transformOffset, instanceCount, pushConstantBuffer]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::RenderMeshWithMaterial"); + SE_SCOPE_PERF("VulkanRenderer::RenderMeshWithMaterial"); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + auto vulkanMeshVB = meshSource->GetVertexBuffer().As(); + VkBuffer vbMeshBuffer = vulkanMeshVB->GetVulkanBuffer(); + VkDeviceSize vertexOffsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vbMeshBuffer, vertexOffsets); + + Ref vulkanTransformBuffer = transformBuffer.As(); + VkBuffer vbTransformBuffer = vulkanTransformBuffer->GetVulkanBuffer(); + VkDeviceSize instanceOffsets[1] = { transformOffset }; + vkCmdBindVertexBuffers(commandBuffer, 1, 1, &vbTransformBuffer, instanceOffsets); + + auto vulkanMeshIB = Ref(meshSource->GetIndexBuffer()); + VkBuffer ibBuffer = vulkanMeshIB->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, ibBuffer, 0, VK_INDEX_TYPE_UINT32); + + Ref vulkanPipeline = pipeline.As(); + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + + // Bind descriptor sets describing shader binding points + // TODO std::vector descriptorSets = resourceSets.As()->GetDescriptorSets(); + // TODO VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + // TODO descriptorSets[0] = descriptorSet; + // TODO vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, (uint32_t)descriptorSets.size(), descriptorSets.data(), 0, nullptr); + + Buffer uniformStorageBuffer = vulkanMaterial->GetUniformStorageBuffer(); + uint32_t pushConstantOffset = 0; + if (pushConstantBuffer.Size) + { + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_VERTEX_BIT, pushConstantOffset, pushConstantBuffer.Size, pushConstantBuffer.Data); + pushConstantOffset += 16; // TODO: it's 16 because that happens to be the offset that is declared for the material push constants in the shaders. Need a better way of doing this. Cannot just use the size of the pushConstantBuffer, because you dont know what alignment the next push constant range might have + } + + if (uniformStorageBuffer) + { + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, pushConstantOffset, uniformStorageBuffer.Size, uniformStorageBuffer.Data); + pushConstantOffset += uniformStorageBuffer.Size; + } + + const auto& submeshes = meshSource->GetSubmeshes(); + const auto& submesh = submeshes[submeshIndex]; + + vkCmdDrawIndexed(commandBuffer, submesh.IndexCount, instanceCount, submesh.BaseIndex, submesh.BaseVertex, 0); + + pushConstantBuffer.Release(); + }); + } + + void VulkanRenderer::RenderQuad(Ref renderCommandBuffer, Ref pipeline, Ref material, const glm::mat4& transform) + { + Ref vulkanMaterial = material.As(); + Renderer::Submit([renderCommandBuffer, pipeline, vulkanMaterial, transform]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::RenderQuad"); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + Ref vulkanPipeline = pipeline.As(); + + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + + auto vulkanMeshVB = s_Data->QuadVertexBuffer.As(); + VkBuffer vbMeshBuffer = vulkanMeshVB->GetVulkanBuffer(); + VkDeviceSize offsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vbMeshBuffer, offsets); + + auto vulkanMeshIB = s_Data->QuadIndexBuffer.As(); + VkBuffer ibBuffer = vulkanMeshIB->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, ibBuffer, 0, VK_INDEX_TYPE_UINT32); + + Buffer uniformStorageBuffer = vulkanMaterial->GetUniformStorageBuffer(); + + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &transform); + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(glm::mat4), uniformStorageBuffer.Size, uniformStorageBuffer.Data); + vkCmdDrawIndexed(commandBuffer, s_Data->QuadIndexBuffer->GetCount(), 1, 0, 0, 0); + }); + } + + void VulkanRenderer::RenderGeometry(Ref renderCommandBuffer, Ref pipeline, Ref material, Ref vertexBuffer, Ref indexBuffer, const glm::mat4& transform, uint32_t indexCount /*= 0*/) + { + Ref vulkanMaterial = material.As(); + if (indexCount == 0) + indexCount = indexBuffer->GetCount(); + + Renderer::Submit([renderCommandBuffer, pipeline, vulkanMaterial, vertexBuffer, indexBuffer, transform, indexCount]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::RenderGeometry"); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + Ref vulkanPipeline = pipeline.As(); + + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + + auto vulkanMeshVB = vertexBuffer.As(); + VkBuffer vbMeshBuffer = vulkanMeshVB->GetVulkanBuffer(); + VkDeviceSize offsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vbMeshBuffer, offsets); + + auto vulkanMeshIB = indexBuffer.As(); + VkBuffer ibBuffer = vulkanMeshIB->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, ibBuffer, 0, VK_INDEX_TYPE_UINT32); + + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descriptorSet, 0, nullptr); + + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &transform); + Buffer uniformStorageBuffer = vulkanMaterial->GetUniformStorageBuffer(); + if (uniformStorageBuffer) + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(glm::mat4), uniformStorageBuffer.Size, uniformStorageBuffer.Data); + + vkCmdDrawIndexed(commandBuffer, indexCount, 1, 0, 0, 0); + }); + } + + VkDescriptorSet VulkanRenderer::RT_AllocateDescriptorSet(VkDescriptorSetAllocateInfo& allocInfo) + { + SE_PROFILE_FUNC(); + + uint32_t bufferIndex = Renderer::RT_GetCurrentFrameIndex(); + allocInfo.descriptorPool = s_Data->DescriptorPools[bufferIndex]; + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + VkDescriptorSet result; + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &result)); + s_Data->DescriptorPoolAllocationCount[bufferIndex] += allocInfo.descriptorSetCount; + return result; + } + + VkDescriptorSet VulkanRenderer::AllocateMaterialDescriptorSet(VkDescriptorSetAllocateInfo& allocInfo) + { + SE_PROFILE_FUNC(); + + uint32_t bufferIndex = Renderer::RT_GetCurrentFrameIndex(); + allocInfo.descriptorPool = s_Data->MaterialDescriptorPool; + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + VkDescriptorSet result; + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &result)); + //s_Data->DescriptorPoolAllocationCount[bufferIndex] += allocInfo.descriptorSetCount; + return result; + } + +#if 0 + void VulkanRenderer::SetUniformBuffer(Ref uniformBuffer, uint32_t set) + { + Renderer::Submit([uniformBuffer, set]() + { + + + SE_CORE_ASSERT(set == 1); // Currently we only bind to Renderer-maintaned UBs, which are in descriptor set 1 + + Ref vulkanUniformBuffer = uniformBuffer.As(); + + VkWriteDescriptorSet writeDescriptorSet = {}; + writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeDescriptorSet.descriptorCount = 1; + writeDescriptorSet.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writeDescriptorSet.pBufferInfo = &vulkanUniformBuffer->GetDescriptorBufferInfo(); + writeDescriptorSet.dstBinding = uniformBuffer->GetBinding(); + writeDescriptorSet.dstSet = s_Data->RendererDescriptorSet.DescriptorSets[0]; + + SE_CORE_WARN("VulkanRenderer - Updating descriptor set (VulkanRenderer::SetUniformBuffer)"); + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + vkUpdateDescriptorSets(device, 1, &writeDescriptorSet, 0, nullptr); + }); + } +#endif + + void VulkanRenderer::ClearImage(Ref commandBuffer, Ref image, const ImageClearValue& clearValue, ImageSubresourceRange subresourceRange) + { + Renderer::Submit([commandBuffer, image = image.As(), clearValue, subresourceRange] + { + const auto vulkanCommandBuffer = commandBuffer.As()->GetCommandBuffer(Renderer::RT_GetCurrentFrameIndex()); + VkImageSubresourceRange vulkanSubresourceRange{}; + vulkanSubresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + vulkanSubresourceRange.baseMipLevel = subresourceRange.BaseMip; + vulkanSubresourceRange.levelCount = subresourceRange.MipCount; + vulkanSubresourceRange.baseArrayLayer = subresourceRange.BaseLayer; + vulkanSubresourceRange.layerCount = subresourceRange.LayerCount; + + vkCmdClearColorImage(vulkanCommandBuffer, image->GetImageInfo().Image, + image->GetSpecification().Usage == ImageUsage::Storage ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + (VkClearColorValue*)&clearValue, 1, &vulkanSubresourceRange); + }); + } + + void VulkanRenderer::CopyImage(Ref commandBuffer, Ref sourceImage, Ref destinationImage) + { + SE_CORE_VERIFY(sourceImage); + SE_CORE_VERIFY(destinationImage); + + Renderer::Submit([commandBuffer, src = sourceImage.As(), dst = destinationImage.As()] + { + const auto vulkanCommandBuffer = commandBuffer.As()->GetCommandBuffer(Renderer::RT_GetCurrentFrameIndex()); + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + VkImage srcImage = src->GetImageInfo().Image; + VkImage dstImage = dst->GetImageInfo().Image; + glm::uvec2 srcSize = src->GetSize(); + glm::uvec2 dstSize = dst->GetSize(); + + VkImageCopy region; + region.srcOffset = { 0, 0, 0 }; + region.dstOffset = { 0, 0, 0 }; + region.extent = { srcSize.x, srcSize.y, 1 }; + region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.srcSubresource.baseArrayLayer = 0; + region.srcSubresource.mipLevel = 0; + region.srcSubresource.layerCount = 1; + region.dstSubresource = region.srcSubresource; + + VkImageLayout srcImageLayout = src->GetDescriptorInfoVulkan().imageLayout; + VkImageLayout dstImageLayout = dst->GetDescriptorInfoVulkan().imageLayout; + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = srcImageLayout; + imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + imageMemoryBarrier.image = srcImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = 0; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = dstImageLayout; + imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + imageMemoryBarrier.image = dstImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = 0; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + vkCmdCopyImage(vulkanCommandBuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + imageMemoryBarrier.newLayout = srcImageLayout; + imageMemoryBarrier.image = srcImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = 0; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + imageMemoryBarrier.newLayout = dstImageLayout; + imageMemoryBarrier.image = dstImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = 0; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + }); + } + + void VulkanRenderer::BlitImage(Ref commandBuffer, Ref sourceImage, Ref destinationImage) + { + SE_CORE_VERIFY(sourceImage); + SE_CORE_VERIFY(destinationImage); + + Renderer::Submit([commandBuffer, src = sourceImage.As(), dst = destinationImage.As()] + { + const auto vulkanCommandBuffer = commandBuffer.As()->GetActiveCommandBuffer(); + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + VkImage srcImage = src->GetImageInfo().Image; + VkImage dstImage = dst->GetImageInfo().Image; + + if (!srcImage || !dstImage) + { + // Can't blit if either image is null + SE_CORE_ERROR_TAG("Renderer", "[VulkanRenderer::BlitImage] Invalid images for blitting! srcImage={} dstImage={}", (uint64_t)srcImage, (uint64_t)dstImage); + return; + } + + glm::uvec2 srcSize = src->GetSize(); + glm::uvec2 dstSize = dst->GetSize(); + int srcMip = 0; + + if (src->HasMips()) + { + // Select lower mip to sample from if we can + srcMip = src->GetClosestMipLevel(dstSize.x, dstSize.y); + auto [mipWidth, mipHeight] = src->GetMipLevelSize(srcMip); + srcSize = { mipWidth, mipHeight }; + } + + VkImageBlit region; + region.srcOffsets[0] = { 0, 0, 0 }; + region.srcOffsets[1] = { (int)srcSize.x, (int)srcSize.y, 1 }; + region.dstOffsets[0] = { 0, 0, 0 }; + region.dstOffsets[1] = { (int)dstSize.x, (int)dstSize.y, 1 }; + region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.srcSubresource.baseArrayLayer = 0; + region.srcSubresource.mipLevel = srcMip; + region.srcSubresource.layerCount = 1; + region.dstSubresource = region.srcSubresource; + region.dstSubresource.mipLevel = 0; + + VkImageLayout srcImageLayout = src->GetDescriptorInfoVulkan().imageLayout; + VkImageLayout dstImageLayout = dst->GetDescriptorInfoVulkan().imageLayout; + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = srcImageLayout; + imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + imageMemoryBarrier.image = srcImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = srcMip; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = dstImageLayout; + imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + imageMemoryBarrier.image = dstImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = 0; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + vkCmdBlitImage(vulkanCommandBuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_LINEAR); + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + imageMemoryBarrier.newLayout = srcImageLayout; + imageMemoryBarrier.image = srcImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = srcMip; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + imageMemoryBarrier.newLayout = dstImageLayout; + imageMemoryBarrier.image = dstImage; + + imageMemoryBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + imageMemoryBarrier.subresourceRange.baseArrayLayer = 0; + imageMemoryBarrier.subresourceRange.baseMipLevel = 0; + imageMemoryBarrier.subresourceRange.layerCount = 1; + imageMemoryBarrier.subresourceRange.levelCount = 1; + + vkCmdPipelineBarrier(vulkanCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + }); + } + + + void VulkanRenderer::LightCulling(Ref renderCommandBuffer, Ref computePass, Ref material, const glm::uvec3& workGroups) + { + auto vulkanMaterial = material.As(); + Renderer::Submit([renderCommandBuffer, computePass, vulkanMaterial, workGroups]() mutable + { + Ref vulkanComputePass = computePass.As(); + Ref pipeline = computePass->GetSpecification().Pipeline.As(); + const uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + + // RT_UpdateMaterialForRendering(vulkanMaterial, uniformBufferSet, storageBufferSet); + + const VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetCommandBuffer(frameIndex); + + // + // Renderer::BeginComputePass + // + { + pipeline->RT_Begin(renderCommandBuffer); // bind pipeline + + // Bind compute pass descriptor set(s) + vulkanComputePass->Prepare(); + const auto& descriptorSets = vulkanComputePass->GetDescriptorSets(frameIndex); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline->GetLayout(), vulkanComputePass->GetFirstSetIndex(), (uint32_t)descriptorSets.size(), descriptorSets.data(), 0, 0); + } + + // + // Renderer::DispatchComputePass + // + { + // Bind material descriptor set + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->GetLayout(), 0, 1, &descriptorSet, 0, nullptr); + + pipeline->Dispatch(workGroups); + } + + VkMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(renderCommandBuffer.As()->GetCommandBuffer(frameIndex), + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, + 1, &barrier, + 0, nullptr, + 0, nullptr); + + // + // Renderer::EndComputePass + // + { + pipeline->End(); + } + }); + } + + void VulkanRenderer::SubmitFullscreenQuad(Ref renderCommandBuffer, Ref pipeline, Ref material) + { + Ref vulkanMaterial = material.As(); + Renderer::Submit([renderCommandBuffer, pipeline, vulkanMaterial]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::SubmitFullscreenQuad"); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + Ref vulkanPipeline = pipeline.As(); + + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + + auto vulkanMeshVB = s_Data->QuadVertexBuffer.As(); + VkBuffer vbMeshBuffer = vulkanMeshVB->GetVulkanBuffer(); + VkDeviceSize offsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vbMeshBuffer, offsets); + + auto vulkanMeshIB = s_Data->QuadIndexBuffer.As(); + VkBuffer ibBuffer = vulkanMeshIB->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, ibBuffer, 0, VK_INDEX_TYPE_UINT32); + + if (vulkanMaterial) + { + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descriptorSet, 0, nullptr); + + Buffer uniformStorageBuffer = vulkanMaterial->GetUniformStorageBuffer(); + if (uniformStorageBuffer.Size) + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, uniformStorageBuffer.Size, uniformStorageBuffer.Data); + } + + vkCmdDrawIndexed(commandBuffer, s_Data->QuadIndexBuffer->GetCount(), 1, 0, 0, 0); + }); + } + + void VulkanRenderer::SubmitFullscreenQuadWithOverrides(Ref renderCommandBuffer, Ref pipeline, Ref material, Buffer vertexShaderOverrides, Buffer fragmentShaderOverrides) + { + Buffer vertexPushConstantBuffer; + if (vertexShaderOverrides) + { + vertexPushConstantBuffer.Allocate(vertexShaderOverrides.Size); + vertexPushConstantBuffer.Write(vertexShaderOverrides.Data, vertexShaderOverrides.Size); + } + + Buffer fragmentPushConstantBuffer; + if (fragmentShaderOverrides) + { + fragmentPushConstantBuffer.Allocate(fragmentShaderOverrides.Size); + fragmentPushConstantBuffer.Write(fragmentShaderOverrides.Data, fragmentShaderOverrides.Size); + } + + Ref vulkanMaterial = material.As(); + Renderer::Submit([renderCommandBuffer, pipeline, vulkanMaterial, vertexPushConstantBuffer, fragmentPushConstantBuffer]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::SubmitFullscreenQuad"); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + Ref vulkanPipeline = pipeline.As(); + + VkPipelineLayout layout = vulkanPipeline->GetVulkanPipelineLayout(); + + auto vulkanMeshVB = s_Data->QuadVertexBuffer.As(); + VkBuffer vbMeshBuffer = vulkanMeshVB->GetVulkanBuffer(); + VkDeviceSize offsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vbMeshBuffer, offsets); + + auto vulkanMeshIB = s_Data->QuadIndexBuffer.As(); + VkBuffer ibBuffer = vulkanMeshIB->GetVulkanBuffer(); + vkCmdBindIndexBuffer(commandBuffer, ibBuffer, 0, VK_INDEX_TYPE_UINT32); + + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descriptorSet, 0, nullptr); + + if (vertexPushConstantBuffer) + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, vertexPushConstantBuffer.Size, vertexPushConstantBuffer.Data); + if (fragmentPushConstantBuffer) + vkCmdPushConstants(commandBuffer, layout, VK_SHADER_STAGE_FRAGMENT_BIT, vertexPushConstantBuffer.Size, fragmentPushConstantBuffer.Size, fragmentPushConstantBuffer.Data); + + vkCmdDrawIndexed(commandBuffer, s_Data->QuadIndexBuffer->GetCount(), 1, 0, 0, 0); + + vertexPushConstantBuffer.Release(); + fragmentPushConstantBuffer.Release(); + }); + } + + void VulkanRenderer::SetSceneEnvironment(Ref sceneRenderer, Ref environment, Ref shadow, Ref spotShadow) + { + if (!environment) + environment = Renderer::GetEmptyEnvironment(); + + Renderer::Submit([sceneRenderer, environment, shadow, spotShadow]() mutable + { + SE_PROFILE_FUNC("VulkanRenderer::SetSceneEnvironment"); + + const auto shader = Renderer::GetShaderLibrary()->Get("StarEnginePBR_Static"); + Ref pbrShader = shader.As(); + const uint32_t bufferIndex = Renderer::RT_GetCurrentFrameIndex(); + + if (s_Data->RendererDescriptorSet.find(sceneRenderer.Raw()) == s_Data->RendererDescriptorSet.end()) + { + const uint32_t framesInFlight = Renderer::GetConfig().FramesInFlight; + s_Data->RendererDescriptorSet[sceneRenderer.Raw()].resize(framesInFlight); + for (uint32_t i = 0; i < framesInFlight; i++) + s_Data->RendererDescriptorSet.at(sceneRenderer.Raw())[i] = pbrShader->CreateDescriptorSets(1); + + } + + VkDescriptorSet descriptorSet = s_Data->RendererDescriptorSet.at(sceneRenderer.Raw())[bufferIndex].DescriptorSets[0]; + s_Data->ActiveRendererDescriptorSet = descriptorSet; + + std::array writeDescriptors; + + Ref radianceMap = environment->RadianceMap.As(); + Ref irradianceMap = environment->IrradianceMap.As(); + + writeDescriptors[0] = *pbrShader->GetDescriptorSet("u_EnvRadianceTex", 1); + writeDescriptors[0].dstSet = descriptorSet; + const auto& radianceMapImageInfo = radianceMap->GetDescriptorInfoVulkan(); + writeDescriptors[0].pImageInfo = &radianceMapImageInfo; + + writeDescriptors[1] = *pbrShader->GetDescriptorSet("u_EnvIrradianceTex", 1); + writeDescriptors[1].dstSet = descriptorSet; + const auto& irradianceMapImageInfo = irradianceMap->GetDescriptorInfoVulkan(); + writeDescriptors[1].pImageInfo = &irradianceMapImageInfo; + + writeDescriptors[2] = *pbrShader->GetDescriptorSet("u_BRDFLUTTexture", 1); + writeDescriptors[2].dstSet = descriptorSet; + const auto& brdfLutImageInfo = s_Data->BRDFLut.As()->GetDescriptorInfoVulkan(); + writeDescriptors[2].pImageInfo = &brdfLutImageInfo; + + writeDescriptors[3] = *pbrShader->GetDescriptorSet("u_ShadowMapTexture", 1); + writeDescriptors[3].dstSet = descriptorSet; + const auto& shadowImageInfo = shadow.As()->GetDescriptorInfoVulkan(); + writeDescriptors[3].pImageInfo = &shadowImageInfo; + + writeDescriptors[4] = *pbrShader->GetDescriptorSet("u_SpotShadowTexture", 1); + writeDescriptors[4].dstSet = descriptorSet; + const auto& spotShadowImageInfo = spotShadow.As()->GetDescriptorInfoVulkan(); + writeDescriptors[4].pImageInfo = &spotShadowImageInfo; + + const auto vulkanDevice = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + vkUpdateDescriptorSets(vulkanDevice, (uint32_t)writeDescriptors.size(), writeDescriptors.data(), 0, nullptr); + }); + } + + void VulkanRenderer::BeginFrame() + { + Renderer::Submit([]() + { + SE_PROFILE_FUNC("VulkanRenderer::BeginFrame"); + + VulkanSwapChain& swapChain = Application::Get().GetWindow().GetSwapChain(); + + // Reset descriptor pools here + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + uint32_t bufferIndex = swapChain.GetCurrentBufferIndex(); + vkResetDescriptorPool(device, s_Data->DescriptorPools[bufferIndex], 0); + memset(s_Data->DescriptorPoolAllocationCount.data(), 0, s_Data->DescriptorPoolAllocationCount.size() * sizeof(uint32_t)); + + s_Data->DrawCallCount = 0; + +#if 0 + VkCommandBufferBeginInfo cmdBufInfo = {}; + cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + cmdBufInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + cmdBufInfo.pNext = nullptr; + + VkCommandBuffer drawCommandBuffer = swapChain.GetCurrentDrawCommandBuffer(); + commandBuffer = drawCommandBuffer; + SE_CORE_ASSERT(commandBuffer); + VK_CHECK_RESULT(vkBeginCommandBuffer(drawCommandBuffer, &cmdBufInfo)); +#endif + }); + } + + void VulkanRenderer::EndFrame() + { +#if 0 + Renderer::Submit([]() + { + VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer)); + commandBuffer = nullptr; + }); +#endif + } + + void VulkanRenderer::InsertGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& color) + { + Renderer::Submit([this, renderCommandBuffer, label, color]() + { + RT_InsertGPUPerfMarker(renderCommandBuffer, label, color); + }); + } + + void VulkanRenderer::BeginGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& markerColor) + { + Renderer::Submit([this, renderCommandBuffer, label, markerColor]() + { + RT_BeginGPUPerfMarker(renderCommandBuffer, label, markerColor); + }); + } + + void VulkanRenderer::EndGPUPerfMarker(Ref renderCommandBuffer) + { + Renderer::Submit([this, renderCommandBuffer]() + { + RT_EndGPUPerfMarker(renderCommandBuffer); + }); + } + + void VulkanRenderer::RT_InsertGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& color) + { + const uint32_t bufferIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetCommandBuffer(bufferIndex); + VkDebugUtilsLabelEXT debugLabel{}; + debugLabel.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + memcpy(&debugLabel.color, glm::value_ptr(color), sizeof(float) * 4); + debugLabel.pLabelName = label.c_str(); + fpCmdInsertDebugUtilsLabelEXT(commandBuffer, &debugLabel); + } + + void VulkanRenderer::RT_BeginGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& markerColor) + { + const uint32_t bufferIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetCommandBuffer(bufferIndex); + VkDebugUtilsLabelEXT debugLabel{}; + debugLabel.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + memcpy(&debugLabel.color, glm::value_ptr(markerColor), sizeof(float) * 4); + debugLabel.pLabelName = label.c_str(); + fpCmdBeginDebugUtilsLabelEXT(commandBuffer, &debugLabel); + } + + void VulkanRenderer::RT_EndGPUPerfMarker(Ref renderCommandBuffer) + { + const uint32_t bufferIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetCommandBuffer(bufferIndex); + fpCmdEndDebugUtilsLabelEXT(commandBuffer); + } + + void VulkanRenderer::BeginRenderPass(Ref renderCommandBuffer, Ref renderPass, bool explicitClear) + { + Renderer::Submit([renderCommandBuffer, renderPass, explicitClear]() + { + SE_PROFILE_SCOPE_DYNAMIC(std::format("VulkanRenderer::BeginRenderPass ({})", renderPass->GetSpecification().DebugName).c_str()); + SE_CORE_TRACE_TAG("Renderer", "BeginRenderPass - {}", renderPass->GetSpecification().DebugName); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + VkDebugUtilsLabelEXT debugLabel{}; + debugLabel.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + memcpy(&debugLabel.color, glm::value_ptr(renderPass->GetSpecification().MarkerColor), sizeof(float) * 4); + debugLabel.pLabelName = renderPass->GetSpecification().DebugName.c_str(); + fpCmdBeginDebugUtilsLabelEXT(commandBuffer, &debugLabel); + + auto fb = renderPass->GetSpecification().Pipeline->GetSpecification().TargetFramebuffer; + Ref framebuffer = fb.As(); + const auto& fbSpec = framebuffer->GetSpecification(); + + uint32_t width = framebuffer->GetWidth(); + uint32_t height = framebuffer->GetHeight(); + + VkViewport viewport = {}; + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + + VkRenderPassBeginInfo renderPassBeginInfo = {}; + renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassBeginInfo.pNext = nullptr; + renderPassBeginInfo.renderPass = framebuffer->GetRenderPass(); + renderPassBeginInfo.renderArea.offset.x = 0; + renderPassBeginInfo.renderArea.offset.y = 0; + renderPassBeginInfo.renderArea.extent.width = width; + renderPassBeginInfo.renderArea.extent.height = height; + if (framebuffer->GetSpecification().SwapChainTarget) + { + VulkanSwapChain& swapChain = Application::Get().GetWindow().GetSwapChain(); + width = swapChain.GetWidth(); + height = swapChain.GetHeight(); + renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassBeginInfo.pNext = nullptr; + renderPassBeginInfo.renderPass = framebuffer->GetRenderPass(); + renderPassBeginInfo.renderArea.offset.x = 0; + renderPassBeginInfo.renderArea.offset.y = 0; + renderPassBeginInfo.renderArea.extent.width = width; + renderPassBeginInfo.renderArea.extent.height = height; + renderPassBeginInfo.framebuffer = swapChain.GetCurrentFramebuffer(); + + viewport.x = 0.0f; + viewport.y = (float)height; + viewport.width = (float)width; + viewport.height = -(float)height; + } + else + { + width = framebuffer->GetWidth(); + height = framebuffer->GetHeight(); + renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassBeginInfo.pNext = nullptr; + renderPassBeginInfo.renderPass = framebuffer->GetRenderPass(); + renderPassBeginInfo.renderArea.offset.x = 0; + renderPassBeginInfo.renderArea.offset.y = 0; + renderPassBeginInfo.renderArea.extent.width = width; + renderPassBeginInfo.renderArea.extent.height = height; + renderPassBeginInfo.framebuffer = framebuffer->GetVulkanFramebuffer(); + + viewport.x = 0.0f; + viewport.y = 0.0f; + viewport.width = (float)width; + viewport.height = (float)height; + } + + // TODO: Does our framebuffer have a depth attachment? + const auto& clearValues = framebuffer->GetVulkanClearValues(); + renderPassBeginInfo.clearValueCount = (uint32_t)clearValues.size(); + renderPassBeginInfo.pClearValues = clearValues.data(); + + vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + + if (explicitClear) + { + const uint32_t colorAttachmentCount = (uint32_t)framebuffer->GetColorAttachmentCount(); + const uint32_t totalAttachmentCount = colorAttachmentCount + (framebuffer->HasDepthAttachment() ? 1 : 0); + SE_CORE_ASSERT(clearValues.size() == totalAttachmentCount); + + std::vector attachments(totalAttachmentCount); + std::vector clearRects(totalAttachmentCount); + for (uint32_t i = 0; i < colorAttachmentCount; i++) + { + attachments[i].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + attachments[i].colorAttachment = i; + attachments[i].clearValue = clearValues[i]; + + clearRects[i].rect.offset = { (int32_t)0, (int32_t)0 }; + clearRects[i].rect.extent = { width, height }; + clearRects[i].baseArrayLayer = 0; + clearRects[i].layerCount = 1; + } + + if (framebuffer->HasDepthAttachment()) + { + attachments[colorAttachmentCount].aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + attachments[colorAttachmentCount].clearValue = clearValues[colorAttachmentCount]; + clearRects[colorAttachmentCount].rect.offset = { (int32_t)0, (int32_t)0 }; + clearRects[colorAttachmentCount].rect.extent = { width, height }; + clearRects[colorAttachmentCount].baseArrayLayer = 0; + clearRects[colorAttachmentCount].layerCount = 1; + } + + vkCmdClearAttachments(commandBuffer, totalAttachmentCount, attachments.data(), totalAttachmentCount, clearRects.data()); + + } + + // Update dynamic viewport state + vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + + // Update dynamic scissor state + VkRect2D scissor = {}; + scissor.extent.width = width; + scissor.extent.height = height; + scissor.offset.x = 0; + scissor.offset.y = 0; + vkCmdSetScissor(commandBuffer, 0, 1, &scissor); + + // TODO: automatic layout transitions for input resources + + // Bind Vulkan Pipeline + Ref vulkanPipeline = renderPass->GetSpecification().Pipeline.As(); + VkPipeline vPipeline = vulkanPipeline->GetVulkanPipeline(); + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, vPipeline); + + if (vulkanPipeline->IsDynamicLineWidth()) + vkCmdSetLineWidth(commandBuffer, vulkanPipeline->GetSpecification().LineWidth); + + // Bind input descriptors (starting from set 1, set 0 is for per-draw) + Ref vulkanRenderPass = renderPass.As(); + vulkanRenderPass->Prepare(); + if (vulkanRenderPass->HasDescriptorSets()) + { + const auto& descriptorSets = vulkanRenderPass->GetDescriptorSets(frameIndex); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, vulkanPipeline->GetVulkanPipelineLayout(), vulkanRenderPass->GetFirstSetIndex(), (uint32_t)descriptorSets.size(), descriptorSets.data(), 0, nullptr); + } + }); + } + + void VulkanRenderer::EndRenderPass(Ref renderCommandBuffer) + { + Renderer::Submit([renderCommandBuffer]() + { + SE_PROFILE_FUNC("VulkanRenderer::EndRenderPass"); + + uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetActiveCommandBuffer(); + + vkCmdEndRenderPass(commandBuffer); + fpCmdEndDebugUtilsLabelEXT(commandBuffer); + }); + } + + void VulkanRenderer::BeginComputePass(Ref renderCommandBuffer, Ref computePass) + { + Renderer::Submit([renderCommandBuffer, computePass]() mutable + { + Ref vulkanComputePass = computePass.As(); + Ref pipeline = computePass->GetSpecification().Pipeline.As(); + + const uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetCommandBuffer(frameIndex); + + VkDebugUtilsLabelEXT debugLabel{}; + debugLabel.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + memcpy(&debugLabel.color, glm::value_ptr(vulkanComputePass->GetSpecification().MarkerColor), sizeof(float) * 4); + debugLabel.pLabelName = vulkanComputePass->GetSpecification().DebugName.c_str(); + fpCmdBeginDebugUtilsLabelEXT(commandBuffer, &debugLabel); + + pipeline->RT_Begin(renderCommandBuffer); // bind pipeline + + // Bind compute pass descriptor set(s) + vulkanComputePass->Prepare(); + if (vulkanComputePass->HasDescriptorSets()) + { + const auto& descriptorSets = vulkanComputePass->GetDescriptorSets(frameIndex); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline->GetLayout(), vulkanComputePass->GetFirstSetIndex(), (uint32_t)descriptorSets.size(), descriptorSets.data(), 0, 0); + } + }); + } + + void VulkanRenderer::EndComputePass(Ref renderCommandBuffer, Ref computePass) + { + Renderer::Submit([renderCommandBuffer, computePass]() mutable + { + Ref vulkanComputePass = computePass.As(); + Ref pipeline = computePass->GetSpecification().Pipeline.As(); + pipeline->End(); + fpCmdEndDebugUtilsLabelEXT(renderCommandBuffer.As()->GetActiveCommandBuffer()); + }); + } + + void VulkanRenderer::DispatchCompute(Ref renderCommandBuffer, Ref computePass, Ref material, const glm::uvec3& workGroups, Buffer constants) + { + Buffer constantsBuffer; + if (constants) + constantsBuffer = Buffer::Copy(constants); + + Renderer::Submit([renderCommandBuffer, computePass, material, workGroups, constantsBuffer]() mutable + { + Ref vulkanComputePass = computePass.As(); + Ref pipeline = computePass->GetSpecification().Pipeline.As(); + + const uint32_t frameIndex = Renderer::RT_GetCurrentFrameIndex(); + VkCommandBuffer commandBuffer = renderCommandBuffer.As()->GetCommandBuffer(frameIndex); + + // Bind material descriptor set if exists + if (material) + { + Ref vulkanMaterial = material.As(); + VkDescriptorSet descriptorSet = vulkanMaterial->GetDescriptorSet(frameIndex); + if (descriptorSet) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline->GetLayout(), 0, 1, &descriptorSet, 0, nullptr); + } + + if (constantsBuffer) + { + pipeline->SetPushConstants(constantsBuffer); + constantsBuffer.Release(); + } + + pipeline->Dispatch(workGroups); + }); + } + + std::pair, Ref> VulkanRenderer::CreateEnvironmentMap(const std::string& filepath) + { + if (!Renderer::GetConfig().ComputeEnvironmentMaps) + return { Renderer::GetBlackCubeTexture(), Renderer::GetBlackCubeTexture() }; + + const uint32_t cubemapSize = Renderer::GetConfig().EnvironmentMapResolution; + const uint32_t irradianceMapSize = 32; + + Ref envEquirect = Texture2D::Create(TextureSpecification(), filepath); + SE_CORE_ASSERT(envEquirect->GetFormat() == ImageFormat::RGBA32F, "Texture is not HDR!"); + + TextureSpecification cubemapSpec; + cubemapSpec.Format = ImageFormat::RGBA16F; + cubemapSpec.Width = cubemapSize; + cubemapSpec.Height = cubemapSize; + + Ref envUnfiltered = TextureCube::Create(cubemapSpec); + Ref envFiltered = TextureCube::Create(cubemapSpec); + + // Convert equirectangular to cubemap + Ref equirectangularConversionShader = Renderer::GetShaderLibrary()->Get("EquirectangularToCubeMap"); + Ref equirectangularConversionPipeline = Ref::Create(equirectangularConversionShader); + + Renderer::Submit([equirectangularConversionPipeline, envEquirect, envUnfiltered, cubemapSize]() mutable + { + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + Ref shader = equirectangularConversionPipeline->GetShader(); + + std::array writeDescriptors; + auto descriptorSet = shader->CreateDescriptorSets(); + Ref envUnfilteredVK = envUnfiltered.As(); + writeDescriptors[0] = *shader->GetDescriptorSet("o_CubeMap"); + writeDescriptors[0].dstSet = descriptorSet.DescriptorSets[0]; // Should this be set inside the shader? + writeDescriptors[0].pImageInfo = &envUnfilteredVK->GetDescriptorInfoVulkan(); + + Ref envEquirectVK = envEquirect.As(); + writeDescriptors[1] = *shader->GetDescriptorSet("u_EquirectangularTex"); + writeDescriptors[1].dstSet = descriptorSet.DescriptorSets[0]; // Should this be set inside the shader? + writeDescriptors[1].pImageInfo = &envEquirectVK->GetDescriptorInfoVulkan(); + + vkUpdateDescriptorSets(device, (uint32_t)writeDescriptors.size(), writeDescriptors.data(), 0, NULL); + equirectangularConversionPipeline->Execute(descriptorSet.DescriptorSets.data(), (uint32_t)descriptorSet.DescriptorSets.size(), cubemapSize / 32, cubemapSize / 32, 6); + + envUnfilteredVK->GenerateMips(true); + }); + + // Copy environment map as-is to filtered mip level 0. This level is used for perfectly reflective materials + Renderer::Submit([equirectangularConversionPipeline, envEquirect, envFiltered, cubemapSize]() mutable + { + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + Ref shader = equirectangularConversionPipeline->GetShader(); + + std::array writeDescriptors; + auto descriptorSet = shader->CreateDescriptorSets(); + Ref envFilteredVK = envFiltered.As(); + writeDescriptors[0] = *shader->GetDescriptorSet("o_CubeMap"); + writeDescriptors[0].dstSet = descriptorSet.DescriptorSets[0]; // Should this be set inside the shader? + writeDescriptors[0].pImageInfo = &envFilteredVK->GetDescriptorInfoVulkan(); + + Ref envEquirectVK = envEquirect.As(); + writeDescriptors[1] = *shader->GetDescriptorSet("u_EquirectangularTex"); + writeDescriptors[1].dstSet = descriptorSet.DescriptorSets[0]; // Should this be set inside the shader? + writeDescriptors[1].pImageInfo = &envEquirectVK->GetDescriptorInfoVulkan(); + + vkUpdateDescriptorSets(device, (uint32_t)writeDescriptors.size(), writeDescriptors.data(), 0, NULL); + equirectangularConversionPipeline->Execute(descriptorSet.DescriptorSets.data(), (uint32_t)descriptorSet.DescriptorSets.size(), cubemapSize / 32, cubemapSize / 32, 6); + }); + + Ref environmentMipFilterShader = Renderer::GetShaderLibrary()->Get("EnvironmentMipFilter"); + Ref environmentMipFilterPipeline = Ref::Create(environmentMipFilterShader); + + Renderer::Submit([environmentMipFilterPipeline, envUnfiltered, envFiltered, cubemapSize]() mutable + { + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + Ref shader = environmentMipFilterPipeline->GetShader(); + + Ref envFilteredCubemap = envFiltered.As(); + VkDescriptorImageInfo imageInfo = envFilteredCubemap->GetDescriptorInfoVulkan(); + + uint32_t mipCount = Utils::CalculateMipCount(cubemapSize, cubemapSize); + + std::vector writeDescriptors(mipCount * 2); + std::vector mipImageInfos(mipCount); + auto descriptorSet = shader->CreateDescriptorSets(0, mipCount); + for (uint32_t i = 0; i < mipCount; i++) + { + VkDescriptorImageInfo& mipImageInfo = mipImageInfos[i]; + mipImageInfo = imageInfo; + mipImageInfo.imageView = envFilteredCubemap->CreateImageViewSingleMip(i); + + writeDescriptors[i * 2 + 0] = *shader->GetDescriptorSet("outputTexture"); + writeDescriptors[i * 2 + 0].dstSet = descriptorSet.DescriptorSets[i]; // Should this be set inside the shader? + writeDescriptors[i * 2 + 0].pImageInfo = &mipImageInfo; + + Ref envUnfilteredCubemap = envUnfiltered.As(); + writeDescriptors[i * 2 + 1] = *shader->GetDescriptorSet("inputTexture"); + writeDescriptors[i * 2 + 1].dstSet = descriptorSet.DescriptorSets[i]; // Should this be set inside the shader? + writeDescriptors[i * 2 + 1].pImageInfo = &envUnfilteredCubemap->GetDescriptorInfoVulkan(); + } + + vkUpdateDescriptorSets(device, (uint32_t)writeDescriptors.size(), writeDescriptors.data(), 0, NULL); + + environmentMipFilterPipeline->RT_Begin(); // begin compute pass + const float deltaRoughness = 1.0f / glm::max((float)envFiltered->GetMipLevelCount() - 1.0f, 1.0f); + + // note: mip level 0 is a copy of the unfiltered env map (see above) + for (uint32_t i = 1, size = cubemapSize; i < mipCount; i++, size /= 2) + { + uint32_t numGroups = glm::max(1u, size / 32); + float roughness = i * deltaRoughness; + //roughness = glm::max(roughness, 0.05f); // not needed, since roughness is always > 0.0f + vkCmdBindDescriptorSets(environmentMipFilterPipeline->GetActiveCommandBuffer(), VK_PIPELINE_BIND_POINT_COMPUTE, environmentMipFilterPipeline->GetLayout(), 0, 1, &descriptorSet.DescriptorSets[i], 0, nullptr); + environmentMipFilterPipeline->SetPushConstants({ &roughness, sizeof(float) }); + environmentMipFilterPipeline->Dispatch({ numGroups, numGroups, 6 }); + } + environmentMipFilterPipeline->End(); + }); + + Ref environmentIrradianceShader = Renderer::GetShaderLibrary()->Get("EnvironmentIrradiance"); + Ref environmentIrradiancePipeline = Ref::Create(environmentIrradianceShader); + + cubemapSpec.Width = irradianceMapSize; + cubemapSpec.Height = irradianceMapSize; + Ref irradianceMap = TextureCube::Create(cubemapSpec); + + Renderer::Submit([environmentIrradiancePipeline, irradianceMap, envFiltered]() mutable + { + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + Ref shader = environmentIrradiancePipeline->GetShader(); + + Ref envFilteredCubemap = envFiltered.As(); + Ref irradianceCubemap = irradianceMap.As(); + auto descriptorSet = shader->CreateDescriptorSets(); + + std::array writeDescriptors; + writeDescriptors[0] = *shader->GetDescriptorSet("o_IrradianceMap"); + writeDescriptors[0].dstSet = descriptorSet.DescriptorSets[0]; + writeDescriptors[0].pImageInfo = &irradianceCubemap->GetDescriptorInfoVulkan(); + + writeDescriptors[1] = *shader->GetDescriptorSet("u_RadianceMap"); + writeDescriptors[1].dstSet = descriptorSet.DescriptorSets[0]; + writeDescriptors[1].pImageInfo = &envFilteredCubemap->GetDescriptorInfoVulkan(); + + vkUpdateDescriptorSets(device, (uint32_t)writeDescriptors.size(), writeDescriptors.data(), 0, NULL); + environmentIrradiancePipeline->RT_Begin(); + vkCmdBindDescriptorSets(environmentIrradiancePipeline->GetActiveCommandBuffer(), VK_PIPELINE_BIND_POINT_COMPUTE, environmentIrradiancePipeline->GetLayout(), 0, 1, &descriptorSet.DescriptorSets[0], 0, nullptr); + environmentIrradiancePipeline->SetPushConstants(Buffer(&Renderer::GetConfig().IrradianceMapComputeSamples, sizeof(uint32_t))); + environmentIrradiancePipeline->Dispatch({ irradianceMap->GetWidth() / 32, irradianceMap->GetHeight() / 32, 6 }); + environmentIrradiancePipeline->End(); + + irradianceCubemap->GenerateMips(); + }); + + return { envFiltered, irradianceMap }; + } + + Ref VulkanRenderer::CreatePreethamSky(float turbidity, float azimuth, float inclination) + { + const uint32_t cubemapSize = Renderer::GetConfig().EnvironmentMapResolution; + const uint32_t irradianceMapSize = 32; + + TextureSpecification cubemapSpec; + cubemapSpec.DebugName = "PreethamSky"; + cubemapSpec.Format = ImageFormat::RGBA32F; + cubemapSpec.Width = cubemapSize; + cubemapSpec.Height = cubemapSize; + + Ref environmentMap = TextureCube::Create(cubemapSpec); + + Ref preethamSkyShader = Renderer::GetShaderLibrary()->Get("PreethamSky"); + Ref preethamSkyComputePipeline = Ref::Create(preethamSkyShader); + + glm::vec3 params = { turbidity, azimuth, inclination }; + Renderer::Submit([preethamSkyComputePipeline, environmentMap, cubemapSize, params]() mutable + { + //VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + Ref shader = preethamSkyComputePipeline->GetShader(); + + std::array writeDescriptors; + auto descriptorSet = shader->CreateDescriptorSets(); + Ref envUnfilteredCubemap = environmentMap.As(); + writeDescriptors[0] = *shader->GetDescriptorSet("o_CubeMap"); + writeDescriptors[0].dstSet = descriptorSet.DescriptorSets[0]; // Should this be set inside the shader? + writeDescriptors[0].pImageInfo = &envUnfilteredCubemap->GetDescriptorInfoVulkan(); + + vkUpdateDescriptorSets(device, (uint32_t)writeDescriptors.size(), writeDescriptors.data(), 0, NULL); + + nvrhi::IDevice* device = Application::Get().GetWindow().GetDeviceManager()->GetDevice(); + + nvrhi::BindingLayoutHandle bindingLayout = shader->GetDescriptorSetLayout(); + nvrhi::BindingSetDesc bindingSetDesc; + bindingSetDesc.bindings = { + nvrhi::BindingSetItem::Texture_SRV(0, environmentMap->GetHandle()) + }; + + nvrhi::BindingSetHandle bindingSet = device->createBindingSet(bindingSetDesc, bindingLayout); + + + // BIND DES descriptorSet.DescriptorSets[0] + preethamSkyComputePipeline->RT_Begin(); + vkCmdBindDescriptorSets(preethamSkyComputePipeline->GetActiveCommandBuffer(), VK_PIPELINE_BIND_POINT_COMPUTE, preethamSkyComputePipeline->GetLayout(), 0, 1, &descriptorSet.DescriptorSets[0], 0, nullptr); + preethamSkyComputePipeline->SetPushConstants(Buffer(¶ms, sizeof(glm::vec3))); + preethamSkyComputePipeline->Dispatch({ cubemapSize / 32, cubemapSize / 32, 6 }); + preethamSkyComputePipeline->End(); + + envUnfilteredCubemap->GenerateMips(true); + }); + + return environmentMap; + } + + uint32_t VulkanRenderer::GetDescriptorAllocationCount(uint32_t frameIndex) + { + return s_Data->DescriptorPoolAllocationCount[frameIndex]; + } + + namespace Utils { + + void InsertImageMemoryBarrier( + VkCommandBuffer cmdbuffer, + VkImage image, + VkAccessFlags srcAccessMask, + VkAccessFlags dstAccessMask, + VkImageLayout oldImageLayout, + VkImageLayout newImageLayout, + VkPipelineStageFlags srcStageMask, + VkPipelineStageFlags dstStageMask, + VkImageSubresourceRange subresourceRange) + { + VkImageMemoryBarrier imageMemoryBarrier{}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + + imageMemoryBarrier.srcAccessMask = srcAccessMask; + imageMemoryBarrier.dstAccessMask = dstAccessMask; + imageMemoryBarrier.oldLayout = oldImageLayout; + imageMemoryBarrier.newLayout = newImageLayout; + imageMemoryBarrier.image = image; + imageMemoryBarrier.subresourceRange = subresourceRange; + + vkCmdPipelineBarrier( + cmdbuffer, + srcStageMask, + dstStageMask, + 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + void SetImageLayout( + VkCommandBuffer cmdbuffer, + VkImage image, + VkImageLayout oldImageLayout, + VkImageLayout newImageLayout, + VkImageSubresourceRange subresourceRange, + VkPipelineStageFlags srcStageMask, + VkPipelineStageFlags dstStageMask) + { + // Create an image barrier object + VkImageMemoryBarrier imageMemoryBarrier = {}; + imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + imageMemoryBarrier.oldLayout = oldImageLayout; + imageMemoryBarrier.newLayout = newImageLayout; + imageMemoryBarrier.image = image; + imageMemoryBarrier.subresourceRange = subresourceRange; + + // Source layouts (old) + // Source access mask controls actions that have to be finished on the old layout + // before it will be transitioned to the new layout + switch (oldImageLayout) + { + case VK_IMAGE_LAYOUT_UNDEFINED: + // Image layout is undefined (or does not matter) + // Only valid as initial layout + // No flags required, listed only for completeness + imageMemoryBarrier.srcAccessMask = 0; + break; + + case VK_IMAGE_LAYOUT_PREINITIALIZED: + // Image is preinitialized + // Only valid as initial layout for linear images, preserves memory contents + // Make sure host writes have been finished + imageMemoryBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + break; + + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + // Image is a color attachment + // Make sure any writes to the color buffer have been finished + imageMemoryBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + break; + + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + // Image is a depth/stencil attachment + // Make sure any writes to the depth/stencil buffer have been finished + imageMemoryBarrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + break; + + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + // Image is a transfer source + // Make sure any reads from the image have been finished + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + break; + + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + // Image is a transfer destination + // Make sure any writes to the image have been finished + imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + break; + + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + // Image is read by a shader + // Make sure any shader reads from the image have been finished + imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + break; + default: + // Other source layouts aren't handled (yet) + break; + } + + // Target layouts (new) + // Destination access mask controls the dependency for the new image layout + switch (newImageLayout) + { + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + // Image will be used as a transfer destination + // Make sure any writes to the image have been finished + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + break; + + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + // Image will be used as a transfer source + // Make sure any reads from the image have been finished + imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + break; + + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + // Image will be used as a color attachment + // Make sure any writes to the color buffer have been finished + imageMemoryBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + break; + + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + // Image layout will be used as a depth/stencil attachment + // Make sure any writes to depth/stencil buffer have been finished + imageMemoryBarrier.dstAccessMask = imageMemoryBarrier.dstAccessMask | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + break; + + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + // Image will be read in a shader (sampler, input attachment) + // Make sure any writes to the image have been finished + if (imageMemoryBarrier.srcAccessMask == 0) + { + imageMemoryBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + } + imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + break; + default: + // Other source layouts aren't handled (yet) + break; + } + + // Put barrier inside setup command buffer + vkCmdPipelineBarrier( + cmdbuffer, + srcStageMask, + dstStageMask, + 0, + 0, nullptr, + 0, nullptr, + 1, &imageMemoryBarrier); + } + + void SetImageLayout( + VkCommandBuffer cmdbuffer, + VkImage image, + VkImageAspectFlags aspectMask, + VkImageLayout oldImageLayout, + VkImageLayout newImageLayout, + VkPipelineStageFlags srcStageMask, + VkPipelineStageFlags dstStageMask) + { + VkImageSubresourceRange subresourceRange = {}; + subresourceRange.aspectMask = aspectMask; + subresourceRange.baseMipLevel = 0; + subresourceRange.levelCount = 1; + subresourceRange.layerCount = 1; + SetImageLayout(cmdbuffer, image, oldImageLayout, newImageLayout, subresourceRange, srcStageMask, dstStageMask); + } + + } + +} + +#endif diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderer.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderer.h new file mode 100644 index 00000000..72db552c --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanRenderer.h @@ -0,0 +1,104 @@ +#pragma once + +#include "VulkanComputePipeline.h" +#include "StarEngine/Renderer/RendererAPI.h" + +#include "VulkanMaterial.h" +#include "StarEngine/Renderer/UniformBuffer.h" +#include "StarEngine/Renderer/StorageBuffer.h" + +#include "vulkan/vulkan.h" + +namespace StarEngine { + +#if TODO + class VulkanRenderer : public RendererAPI + { + public: + virtual void Init() override; + virtual void Shutdown() override; + + virtual RendererCapabilities& GetCapabilities() override; + + virtual void BeginFrame() override; + virtual void EndFrame() override; + + virtual void InsertGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& markerColor = {}) override; + virtual void BeginGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& markerColor = {}) override; + virtual void EndGPUPerfMarker(Ref renderCommandBuffer) override; + + virtual void RT_InsertGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& markerColor = {}) override; + virtual void RT_BeginGPUPerfMarker(Ref renderCommandBuffer, const std::string& label, const glm::vec4& markerColor = {}) override; + virtual void RT_EndGPUPerfMarker(Ref renderCommandBuffer) override; + + virtual void BeginRenderPass(Ref renderCommandBuffer, Ref renderPass, bool explicitClear = false) override; + virtual void EndRenderPass(Ref renderCommandBuffer) override; + + virtual void BeginComputePass(Ref renderCommandBuffer, Ref computePass) override; + virtual void EndComputePass(Ref renderCommandBuffer, Ref computePass) override; + virtual void DispatchCompute(Ref renderCommandBuffer, Ref computePass, Ref material, const glm::uvec3& workGroups, Buffer constants) override; + + virtual void SubmitFullscreenQuad(Ref renderCommandBuffer, Ref pipeline, Ref material) override; + virtual void SubmitFullscreenQuadWithOverrides(Ref renderCommandBuffer, Ref pipeline, Ref material, Buffer vertexShaderOverrides, Buffer fragmentShaderOverrides) override; + + virtual void SetSceneEnvironment(Ref sceneRenderer, Ref environment, Ref shadow, Ref spotShadow) override; + virtual std::pair, Ref> CreateEnvironmentMap(const std::string& filepath) override; + virtual Ref CreatePreethamSky(float turbidity, float azimuth, float inclination) override; + + virtual void RenderStaticMesh(Ref renderCommandBuffer, Ref pipeline, Ref mesh, Ref meshSource, uint32_t submeshIndex, Ref materialTable, Ref transformBuffer, uint32_t transformOffset, uint32_t instanceCount) override; + virtual void RenderSubmeshInstanced(Ref renderCommandBuffer, Ref pipeline, Ref mesh, Ref meshSource, uint32_t index, Ref materialTable, Ref transformBuffer, uint32_t transformOffset, uint32_t boneTransformsOffset, uint32_t boneTransformsStride, uint32_t instanceCount) override; + virtual void RenderMeshWithMaterial(Ref renderCommandBuffer, Ref pipeline, Ref mesh, Ref meshSource, uint32_t submeshIndex, Ref material, Ref transformBuffer, uint32_t transformOffset, uint32_t boneTransformsOffset, uint32_t boneTransformsStride, uint32_t instanceCount, Buffer additionalUniforms = Buffer()) override; + virtual void RenderStaticMeshWithMaterial(Ref renderCommandBuffer, Ref pipeline, Ref mesh, Ref meshSource, uint32_t submeshIndex, Ref material, Ref transformBuffer, uint32_t transformOffset, uint32_t instanceCount, Buffer additionalUniforms = Buffer()) override; + virtual void RenderQuad(Ref renderCommandBuffer, Ref pipeline, Ref material, const glm::mat4& transform) override; + virtual void LightCulling(Ref renderCommandBuffer, Ref computePass, Ref material, const glm::uvec3& workGroups) override; + virtual void RenderGeometry(Ref renderCommandBuffer, Ref pipeline, Ref material, Ref vertexBuffer, Ref indexBuffer, const glm::mat4& transform, uint32_t indexCount = 0) override; + virtual void ClearImage(Ref commandBuffer, Ref image, const ImageClearValue& clearValue, ImageSubresourceRange subresourceRange) override; + virtual void CopyImage(Ref commandBuffer, Ref sourceImage, Ref destinationImage) override; + virtual void BlitImage(Ref commandBuffer, Ref sourceImage, Ref destinationImage) override; + + static VkSampler GetClampSampler(); + static VkSampler GetPointSampler(); + + static uint32_t GetDescriptorAllocationCount(uint32_t frameIndex = 0); + + static int32_t& GetSelectedDrawCall(); + public: + static VkDescriptorSet RT_AllocateDescriptorSet(VkDescriptorSetAllocateInfo& allocInfo); + static VkDescriptorSet AllocateMaterialDescriptorSet(VkDescriptorSetAllocateInfo& allocInfo); + }; + + namespace Utils { + + void InsertImageMemoryBarrier( + VkCommandBuffer cmdbuffer, + VkImage image, + VkAccessFlags srcAccessMask, + VkAccessFlags dstAccessMask, + VkImageLayout oldImageLayout, + VkImageLayout newImageLayout, + VkPipelineStageFlags srcStageMask, + VkPipelineStageFlags dstStageMask, + VkImageSubresourceRange subresourceRange); + + void SetImageLayout( + VkCommandBuffer cmdbuffer, + VkImage image, + VkImageLayout oldImageLayout, + VkImageLayout newImageLayout, + VkImageSubresourceRange subresourceRange, + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); + + void SetImageLayout( + VkCommandBuffer cmdbuffer, + VkImage image, + VkImageAspectFlags aspectMask, + VkImageLayout oldImageLayout, + VkImageLayout newImageLayout, + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); + + } +#endif + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShader.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShader.cpp new file mode 100644 index 00000000..cdeb8b8f --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShader.cpp @@ -0,0 +1,742 @@ +#include "sepch.h" +#include "VulkanShader.h" + +#include "VulkanShaderUtils.h" + +#if SE_HAS_SHADER_COMPILER +#include "ShaderCompiler/VulkanShaderCompiler.h" +#endif + +#include "StarEngine/Core/Hash.h" +#include "StarEngine/ImGui/PropertyGrid.h" +#include "StarEngine/Platform/Vulkan/VulkanContext.h" +#include "StarEngine/Renderer/Renderer.h" +#include "StarEngine/Utilities/StringUtils.h" + +#include +#include + +namespace StarEngine { + + VulkanShader::VulkanShader(const std::string& path, bool forceCompile, bool disableOptimization) + : m_AssetPath(path), m_DisableOptimization(disableOptimization) + { + // TODO: This should be more "general" + size_t found = path.find_last_of("/\\"); + m_Name = found != std::string::npos ? path.substr(found + 1) : path; + found = m_Name.find_last_of('.'); + m_Name = found != std::string::npos ? m_Name.substr(0, found) : m_Name; + + Reload(forceCompile); + } + + void VulkanShader::Release() + { + auto& pipelineCIs = m_PipelineShaderStageCreateInfos; + Renderer::SubmitResourceFree([pipelineCIs]() + { + const auto vulkanDevice = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + for (const auto& ci : pipelineCIs) + if (ci.module) + vkDestroyShaderModule(vulkanDevice, ci.module, nullptr); + }); + + for (auto& ci : pipelineCIs) + ci.module = nullptr; + + m_PipelineShaderStageCreateInfos.clear(); + m_DescriptorSetLayouts.resize(0); + m_TypeCounts.clear(); + } + + VulkanShader::~VulkanShader() + { + VkDevice device = VulkanContext::Get()->GetDevice()->GetVulkanDevice(); + Renderer::SubmitResourceFree([device, instance = Ref(this)]() + { + for (const auto& ci : instance->m_PipelineShaderStageCreateInfos) + if (ci.module) + vkDestroyShaderModule(device, ci.module, nullptr); + }); + } + + void VulkanShader::RT_Reload(const bool forceCompile) + { +#if SE_HAS_SHADER_COMPILER + if (!VulkanShaderCompiler::TryRecompile(this)) + { + SE_CORE_FATAL("Failed to recompile shader!"); + } +#endif + } + + void VulkanShader::Reload(bool forceCompile) + { + Renderer::Submit([instance = Ref(this), forceCompile]() mutable + { + instance->RT_Reload(forceCompile); + }); + } + + size_t VulkanShader::GetHash() const + { + return Hash::GenerateFNVHash(m_AssetPath.string()); + } + + void VulkanShader::LoadAndCreateShaders(const std::map>& shaderData) + { + m_ShaderData = shaderData; + + nvrhi::IDevice* device = Application::Get().GetWindow().GetDeviceManager()->GetDevice(); +#if OLD + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + m_PipelineShaderStageCreateInfos.clear(); +#endif + m_ShaderHandles.clear(); + + std::string moduleName; + for (auto [stage, data] : shaderData) + { + SE_CORE_ASSERT(data.size()); + +#if OLD + VkShaderModuleCreateInfo moduleCreateInfo{}; + + moduleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + moduleCreateInfo.codeSize = data.size() * sizeof(uint32_t); + moduleCreateInfo.pCode = data.data(); + + VkShaderModule shaderModule; + VK_CHECK_RESULT(vkCreateShaderModule(device, &moduleCreateInfo, NULL, &shaderModule)); + VKUtils::SetDebugUtilsObjectName(device, VK_OBJECT_TYPE_SHADER_MODULE, std::format("{}:{}", m_Name, ShaderUtils::ShaderStageToString(stage)), shaderModule); + + VkPipelineShaderStageCreateInfo& shaderStage = m_PipelineShaderStageCreateInfos.emplace_back(); + shaderStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + shaderStage.stage = stage; + shaderStage.module = shaderModule; + shaderStage.pName = "main"; +#endif + + nvrhi::ShaderDesc desc; + desc.shaderType = stage; + desc.debugName = m_Name; + desc.entryName = "main"; + m_ShaderHandles[stage] = device->createShader(desc, data.data(), data.size() * sizeof(uint32_t)); + } + } + + + + void VulkanShader::CreateDescriptors() + { + //SE_CORE_VERIFY(false); +#if OLD + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + ////////////////////////////////////////////////////////////////////// + // Descriptor Pool + ////////////////////////////////////////////////////////////////////// + + m_TypeCounts.clear(); + for (uint32_t set = 0; set < m_ReflectionData.ShaderDescriptorSets.size(); set++) + { + auto& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[set]; + + if (shaderDescriptorSet.UniformBuffers.size()) + { + VkDescriptorPoolSize& typeCount = m_TypeCounts[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + typeCount.descriptorCount = (uint32_t)(shaderDescriptorSet.UniformBuffers.size()); + } + if (shaderDescriptorSet.StorageBuffers.size()) + { + VkDescriptorPoolSize& typeCount = m_TypeCounts[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + typeCount.descriptorCount = (uint32_t)(shaderDescriptorSet.StorageBuffers.size()); + } + if (shaderDescriptorSet.ImageSamplers.size()) + { + VkDescriptorPoolSize& typeCount = m_TypeCounts[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + typeCount.descriptorCount = (uint32_t)(shaderDescriptorSet.ImageSamplers.size()); + } + if (shaderDescriptorSet.SeparateTextures.size()) + { + VkDescriptorPoolSize& typeCount = m_TypeCounts[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + typeCount.descriptorCount = (uint32_t)(shaderDescriptorSet.SeparateTextures.size()); + } + if (shaderDescriptorSet.SeparateSamplers.size()) + { + VkDescriptorPoolSize& typeCount = m_TypeCounts[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_SAMPLER; + typeCount.descriptorCount = (uint32_t)(shaderDescriptorSet.SeparateSamplers.size()); + } + if (shaderDescriptorSet.StorageImages.size()) + { + VkDescriptorPoolSize& typeCount = m_TypeCounts[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + typeCount.descriptorCount = (uint32_t)(shaderDescriptorSet.StorageImages.size()); + } + +#if 0 + // TODO: Move this to the centralized renderer + VkDescriptorPoolCreateInfo descriptorPoolInfo = {}; + descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + descriptorPoolInfo.pNext = nullptr; + descriptorPoolInfo.poolSizeCount = m_TypeCounts.size(); + descriptorPoolInfo.pPoolSizes = m_TypeCounts.data(); + descriptorPoolInfo.maxSets = 1; + + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &m_DescriptorPool)); +#endif + + ////////////////////////////////////////////////////////////////////// + // Descriptor Set Layout + ////////////////////////////////////////////////////////////////////// + + + std::vector layoutBindings; + for (auto& [binding, uniformBuffer] : shaderDescriptorSet.UniformBuffers) + { + VkDescriptorSetLayoutBinding& layoutBinding = layoutBindings.emplace_back(); + layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + layoutBinding.descriptorCount = 1; + layoutBinding.stageFlags = uniformBuffer.ShaderStage; + layoutBinding.pImmutableSamplers = nullptr; + layoutBinding.binding = binding; + + VkWriteDescriptorSet& writeDescriptorSet = shaderDescriptorSet.WriteDescriptorSets[uniformBuffer.Name]; + writeDescriptorSet = {}; + writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeDescriptorSet.descriptorType = layoutBinding.descriptorType; + writeDescriptorSet.descriptorCount = 1; + writeDescriptorSet.dstBinding = layoutBinding.binding; + } + + for (auto& [binding, storageBuffer] : shaderDescriptorSet.StorageBuffers) + { + VkDescriptorSetLayoutBinding& layoutBinding = layoutBindings.emplace_back(); + layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layoutBinding.descriptorCount = 1; + layoutBinding.stageFlags = storageBuffer.ShaderStage; + layoutBinding.pImmutableSamplers = nullptr; + layoutBinding.binding = binding; + SE_CORE_ASSERT(shaderDescriptorSet.UniformBuffers.find(binding) == shaderDescriptorSet.UniformBuffers.end(), "Binding is already present!"); + + VkWriteDescriptorSet& writeDescriptorSet = shaderDescriptorSet.WriteDescriptorSets[storageBuffer.Name]; + writeDescriptorSet = {}; + writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeDescriptorSet.descriptorType = layoutBinding.descriptorType; + writeDescriptorSet.descriptorCount = 1; + writeDescriptorSet.dstBinding = layoutBinding.binding; + } + + for (auto& [binding, imageSampler] : shaderDescriptorSet.ImageSamplers) + { + auto& layoutBinding = layoutBindings.emplace_back(); + layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + layoutBinding.descriptorCount = imageSampler.ArraySize; + layoutBinding.stageFlags = imageSampler.ShaderStage; + layoutBinding.pImmutableSamplers = nullptr; + layoutBinding.binding = binding; + + SE_CORE_ASSERT(shaderDescriptorSet.UniformBuffers.find(binding) == shaderDescriptorSet.UniformBuffers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.StorageBuffers.find(binding) == shaderDescriptorSet.StorageBuffers.end(), "Binding is already present!"); + + VkWriteDescriptorSet& writeDescriptorSet = shaderDescriptorSet.WriteDescriptorSets[imageSampler.Name]; + writeDescriptorSet = {}; + writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeDescriptorSet.descriptorType = layoutBinding.descriptorType; + writeDescriptorSet.descriptorCount = layoutBinding.descriptorCount; + writeDescriptorSet.dstBinding = layoutBinding.binding; + } + + for (auto& [binding, imageSampler] : shaderDescriptorSet.SeparateTextures) + { + auto& layoutBinding = layoutBindings.emplace_back(); + layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + layoutBinding.descriptorCount = imageSampler.ArraySize; + layoutBinding.stageFlags = imageSampler.ShaderStage; + layoutBinding.pImmutableSamplers = nullptr; + layoutBinding.binding = binding; + + SE_CORE_ASSERT(shaderDescriptorSet.UniformBuffers.find(binding) == shaderDescriptorSet.UniformBuffers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.ImageSamplers.find(binding) == shaderDescriptorSet.ImageSamplers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.StorageBuffers.find(binding) == shaderDescriptorSet.StorageBuffers.end(), "Binding is already present!"); + + VkWriteDescriptorSet& writeDescriptorSet = shaderDescriptorSet.WriteDescriptorSets[imageSampler.Name]; + writeDescriptorSet = {}; + writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeDescriptorSet.descriptorType = layoutBinding.descriptorType; + writeDescriptorSet.descriptorCount = imageSampler.ArraySize; + writeDescriptorSet.dstBinding = layoutBinding.binding; + } + + for (auto& [binding, imageSampler] : shaderDescriptorSet.SeparateSamplers) + { + auto& layoutBinding = layoutBindings.emplace_back(); + layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + layoutBinding.descriptorCount = imageSampler.ArraySize; + layoutBinding.stageFlags = imageSampler.ShaderStage; + layoutBinding.pImmutableSamplers = nullptr; + layoutBinding.binding = binding; + + SE_CORE_ASSERT(shaderDescriptorSet.UniformBuffers.find(binding) == shaderDescriptorSet.UniformBuffers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.ImageSamplers.find(binding) == shaderDescriptorSet.ImageSamplers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.StorageBuffers.find(binding) == shaderDescriptorSet.StorageBuffers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.SeparateTextures.find(binding) == shaderDescriptorSet.SeparateTextures.end(), "Binding is already present!"); + + VkWriteDescriptorSet& writeDescriptorSet = shaderDescriptorSet.WriteDescriptorSets[imageSampler.Name]; + writeDescriptorSet = {}; + writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeDescriptorSet.descriptorType = layoutBinding.descriptorType; + writeDescriptorSet.descriptorCount = imageSampler.ArraySize; + writeDescriptorSet.dstBinding = layoutBinding.binding; + } + + for (auto& [bindingAndSet, imageSampler] : shaderDescriptorSet.StorageImages) + { + auto& layoutBinding = layoutBindings.emplace_back(); + layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + layoutBinding.descriptorCount = imageSampler.ArraySize; + layoutBinding.stageFlags = imageSampler.ShaderStage; + layoutBinding.pImmutableSamplers = nullptr; + + uint32_t binding = bindingAndSet & 0xffffffff; + //uint32_t descriptorSet = (bindingAndSet >> 32); + layoutBinding.binding = binding; + + SE_CORE_ASSERT(shaderDescriptorSet.UniformBuffers.find(binding) == shaderDescriptorSet.UniformBuffers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.StorageBuffers.find(binding) == shaderDescriptorSet.StorageBuffers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.ImageSamplers.find(binding) == shaderDescriptorSet.ImageSamplers.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.SeparateTextures.find(binding) == shaderDescriptorSet.SeparateTextures.end(), "Binding is already present!"); + SE_CORE_ASSERT(shaderDescriptorSet.SeparateSamplers.find(binding) == shaderDescriptorSet.SeparateSamplers.end(), "Binding is already present!"); + + VkWriteDescriptorSet& writeDescriptorSet = shaderDescriptorSet.WriteDescriptorSets[imageSampler.Name]; + writeDescriptorSet = {}; + writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writeDescriptorSet.descriptorType = layoutBinding.descriptorType; + writeDescriptorSet.descriptorCount = layoutBinding.descriptorCount; + writeDescriptorSet.dstBinding = layoutBinding.binding; + } + + VkDescriptorSetLayoutCreateInfo descriptorLayout = {}; + descriptorLayout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + descriptorLayout.pNext = nullptr; + descriptorLayout.bindingCount = (uint32_t)(layoutBindings.size()); + descriptorLayout.pBindings = layoutBindings.data(); + + SE_CORE_TRACE_TAG("Renderer", "Creating descriptor set {0} with {1} ubo's, {2} ssbo's, {3} samplers, {4} separate textures, {5} separate samplers and {6} storage images", set, + shaderDescriptorSet.UniformBuffers.size(), + shaderDescriptorSet.StorageBuffers.size(), + shaderDescriptorSet.ImageSamplers.size(), + shaderDescriptorSet.SeparateTextures.size(), + shaderDescriptorSet.SeparateSamplers.size(), + shaderDescriptorSet.StorageImages.size()); + if (set >= m_DescriptorSetLayouts.size()) + m_DescriptorSetLayouts.resize((size_t)(set + 1)); + VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &m_DescriptorSetLayouts[set])); + } +#endif + nvrhi::DeviceHandle device = Application::GetGraphicsDevice(); + + m_DescriptorSetLayouts.resize(m_ReflectionData.ShaderDescriptorSets.size()); + for (uint32_t set = 0; set < m_ReflectionData.ShaderDescriptorSets.size(); set++) + { + auto& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[set]; + + std::vector layoutBindings; + + nvrhi::BindingLayoutDesc bindingLayoutDesc; + bindingLayoutDesc.visibility = nvrhi::ShaderType::None; + + if (set == 0 && !m_ReflectionData.PushConstantRanges.empty()) + { + uint32_t index = 0; + for (const ShaderResource::PushConstantRange& pushConstantRange : m_ReflectionData.PushConstantRanges) + { + bindingLayoutDesc.visibility |= pushConstantRange.ShaderStage; + bindingLayoutDesc.bindings.push_back(nvrhi::BindingLayoutItem::PushConstants(index++, pushConstantRange.Size)); + } + } + + for (auto& [binding, uniformBuffer] : shaderDescriptorSet.UniformBuffers) + { + RenderInputDeclaration& inputDecl = shaderDescriptorSet.InputDeclarations[uniformBuffer.Name]; + inputDecl.Type = RenderInputType::UniformBuffer; + inputDecl.Set = set; + inputDecl.Binding = binding; + inputDecl.Name = uniformBuffer.Name; + inputDecl.Count = 1; + + bindingLayoutDesc.visibility |= uniformBuffer.ShaderStage; + bindingLayoutDesc.bindings.push_back(nvrhi::BindingLayoutItem::ConstantBuffer(binding)); + } + + for (auto& [binding, storageBuffer] : shaderDescriptorSet.StorageBuffers) + { + RenderInputDeclaration& inputDecl = shaderDescriptorSet.InputDeclarations[storageBuffer.Name]; + inputDecl.Type = RenderInputType::StorageBuffer; + inputDecl.Set = set; + inputDecl.Binding = binding; + inputDecl.Name = storageBuffer.Name; + inputDecl.Count = 1; + + bindingLayoutDesc.visibility |= storageBuffer.ShaderStage; + bindingLayoutDesc.bindings.push_back(nvrhi::BindingLayoutItem::RawBuffer_UAV(binding)); + } + + for (auto& [binding, imageSampler] : shaderDescriptorSet.ImageSamplers) + { + RenderInputDeclaration& inputDecl = shaderDescriptorSet.InputDeclarations[imageSampler.Name]; + switch (imageSampler.Dimension) + { + case 1: + inputDecl.Type = RenderInputType::ImageSampler1D; + break; + case 2: + inputDecl.Type = RenderInputType::ImageSampler2D; + break; + case 3: + inputDecl.Type = RenderInputType::ImageSampler3D; + break; + default: + SE_CORE_ASSERT(false); + } + + inputDecl.Set = set; + inputDecl.Binding = binding; + inputDecl.Name = imageSampler.Name; + inputDecl.Count = imageSampler.ArraySize; + + nvrhi::BindingLayoutItem bindingLayoutItem = nvrhi::BindingLayoutItem::Texture_SRV(binding); + bindingLayoutItem.size = imageSampler.ArraySize; + + bindingLayoutDesc.visibility |= imageSampler.ShaderStage; + bindingLayoutDesc.bindings.push_back(bindingLayoutItem); + } + + for (auto& [binding, imageSampler] : shaderDescriptorSet.SeparateTextures) + { + RenderInputDeclaration& inputDecl = shaderDescriptorSet.InputDeclarations[imageSampler.Name]; + switch (imageSampler.Dimension) + { + case 1: + inputDecl.Type = RenderInputType::ImageSampler1D; + break; + case 2: + inputDecl.Type = RenderInputType::ImageSampler2D; + break; + case 3: + inputDecl.Type = RenderInputType::ImageSampler3D; + break; + default: + SE_CORE_ASSERT(false); + + } + inputDecl.Set = set; + inputDecl.Binding = binding; + inputDecl.Name = imageSampler.Name; + + inputDecl.Count = imageSampler.ArraySize; + + nvrhi::BindingLayoutItem bindingLayoutItem = nvrhi::BindingLayoutItem::Texture_SRV(binding); + bindingLayoutItem.size = imageSampler.ArraySize; + + bindingLayoutDesc.visibility |= imageSampler.ShaderStage; + bindingLayoutDesc.bindings.push_back(bindingLayoutItem); + } + + for (auto& [binding, imageSampler] : shaderDescriptorSet.SeparateSamplers) + { + RenderInputDeclaration& inputDecl = shaderDescriptorSet.InputDeclarations[imageSampler.Name]; + switch (imageSampler.Dimension) + { + case 0: + inputDecl.Type = RenderInputType::ImageSampler; + break; + case 1: + inputDecl.Type = RenderInputType::ImageSampler1D; + break; + case 2: + inputDecl.Type = RenderInputType::ImageSampler2D; + break; + case 3: + inputDecl.Type = RenderInputType::ImageSampler3D; + break; + default: + SE_CORE_ASSERT(false); + + } + + inputDecl.Set = set; + inputDecl.Binding = binding; + inputDecl.Name = imageSampler.Name; + inputDecl.Count = imageSampler.ArraySize; + + nvrhi::BindingLayoutItem bindingLayoutItem = nvrhi::BindingLayoutItem::Sampler(binding); + bindingLayoutItem.size = imageSampler.ArraySize; + + bindingLayoutDesc.visibility |= imageSampler.ShaderStage; + bindingLayoutDesc.bindings.push_back(bindingLayoutItem); + } + + for (auto& [binding, imageSampler] : shaderDescriptorSet.StorageImages) + { + RenderInputDeclaration& inputDecl = shaderDescriptorSet.InputDeclarations[imageSampler.Name]; + switch (imageSampler.Dimension) + { + case 1: + inputDecl.Type = RenderInputType::StorageImage1D; + break; + case 2: + inputDecl.Type = RenderInputType::StorageImage2D; + break; + case 3: + inputDecl.Type = RenderInputType::StorageImage3D; + break; + default: + SE_CORE_ASSERT(false); + + } + + inputDecl.Set = set; + inputDecl.Binding = binding; + inputDecl.Name = imageSampler.Name; + inputDecl.Count = imageSampler.ArraySize; + + nvrhi::BindingLayoutItem bindingLayoutItem = nvrhi::BindingLayoutItem::Texture_UAV(binding); + bindingLayoutItem.size = imageSampler.ArraySize; + + bindingLayoutDesc.visibility |= imageSampler.ShaderStage; + bindingLayoutDesc.bindings.push_back(bindingLayoutItem); + } + + m_DescriptorSetLayouts[set] = device->createBindingLayout(bindingLayoutDesc); + } + + } + +#if OLD + + VulkanShader::ShaderMaterialDescriptorSet VulkanShader::AllocateDescriptorSet(uint32_t set) + { + SE_CORE_ASSERT(set < m_DescriptorSetLayouts.size()); + ShaderMaterialDescriptorSet result; + + if (m_ReflectionData.ShaderDescriptorSets.empty()) + return result; + + // TODO: remove + result.Pool = nullptr; + + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &m_DescriptorSetLayouts[set]; + VkDescriptorSet descriptorSet = VulkanRenderer::RT_AllocateDescriptorSet(allocInfo); + SE_CORE_ASSERT(descriptorSet); + result.DescriptorSets.push_back(descriptorSet); + return result; + } + + VulkanShader::ShaderMaterialDescriptorSet VulkanShader::CreateDescriptorSets(uint32_t set) + { + ShaderMaterialDescriptorSet result; + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + SE_CORE_ASSERT(m_TypeCounts.find(set) != m_TypeCounts.end()); + + // TODO: Move this to the centralized renderer + VkDescriptorPoolCreateInfo descriptorPoolInfo = {}; + descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + descriptorPoolInfo.pNext = nullptr; + descriptorPoolInfo.poolSizeCount = (uint32_t)m_TypeCounts.at(set).size(); + descriptorPoolInfo.pPoolSizes = m_TypeCounts.at(set).data(); + descriptorPoolInfo.maxSets = 1; + + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &result.Pool)); + + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = result.Pool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &m_DescriptorSetLayouts[set]; + + result.DescriptorSets.emplace_back(); + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, result.DescriptorSets.data())); + return result; + } + + VulkanShader::ShaderMaterialDescriptorSet VulkanShader::CreateDescriptorSets(uint32_t set, uint32_t numberOfSets) + { + ShaderMaterialDescriptorSet result; + + VkDevice device = VulkanContext::GetCurrentDevice()->GetVulkanDevice(); + + std::unordered_map> poolSizes; + for (uint32_t set = 0; set < m_ReflectionData.ShaderDescriptorSets.size(); set++) + { + auto& shaderDescriptorSet = m_ReflectionData.ShaderDescriptorSets[set]; + if (!shaderDescriptorSet) // Empty descriptor set + continue; + + if (shaderDescriptorSet.UniformBuffers.size()) + { + VkDescriptorPoolSize& typeCount = poolSizes[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + typeCount.descriptorCount = (uint32_t)shaderDescriptorSet.UniformBuffers.size() * numberOfSets; + } + if (shaderDescriptorSet.StorageBuffers.size()) + { + VkDescriptorPoolSize& typeCount = poolSizes[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + typeCount.descriptorCount = (uint32_t)shaderDescriptorSet.StorageBuffers.size() * numberOfSets; + } + if (shaderDescriptorSet.ImageSamplers.size()) + { + VkDescriptorPoolSize& typeCount = poolSizes[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + uint32_t descriptorSetCount = 0; + for (auto&& [binding, imageSampler] : shaderDescriptorSet.ImageSamplers) + descriptorSetCount += imageSampler.ArraySize; + + typeCount.descriptorCount = descriptorSetCount * numberOfSets; + } + if (shaderDescriptorSet.SeparateTextures.size()) + { + VkDescriptorPoolSize& typeCount = poolSizes[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + uint32_t descriptorSetCount = 0; + for (auto&& [binding, imageSampler] : shaderDescriptorSet.SeparateTextures) + descriptorSetCount += imageSampler.ArraySize; + + typeCount.descriptorCount = descriptorSetCount * numberOfSets; + } + if (shaderDescriptorSet.SeparateTextures.size()) + { + VkDescriptorPoolSize& typeCount = poolSizes[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_SAMPLER; + uint32_t descriptorSetCount = 0; + for (auto&& [binding, imageSampler] : shaderDescriptorSet.SeparateSamplers) + descriptorSetCount += imageSampler.ArraySize; + + typeCount.descriptorCount = descriptorSetCount * numberOfSets; + } + if (shaderDescriptorSet.StorageImages.size()) + { + VkDescriptorPoolSize& typeCount = poolSizes[set].emplace_back(); + typeCount.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + typeCount.descriptorCount = (uint32_t)shaderDescriptorSet.StorageImages.size() * numberOfSets; + } + + } + + SE_CORE_ASSERT(poolSizes.find(set) != poolSizes.end()); + + // TODO: Move this to the centralized renderer + VkDescriptorPoolCreateInfo descriptorPoolInfo = {}; + descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + descriptorPoolInfo.pNext = nullptr; + descriptorPoolInfo.poolSizeCount = (uint32_t)poolSizes.at(set).size(); + descriptorPoolInfo.pPoolSizes = poolSizes.at(set).data(); + descriptorPoolInfo.maxSets = numberOfSets; + + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &result.Pool)); + + result.DescriptorSets.resize(numberOfSets); + + for (uint32_t i = 0; i < numberOfSets; i++) + { + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = result.Pool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &m_DescriptorSetLayouts[set]; + + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &result.DescriptorSets[i])); + } + return result; + } + + const VkWriteDescriptorSet* VulkanShader::GetDescriptorSet(const std::string& name, uint32_t set) const + { + SE_CORE_ASSERT(set < m_ReflectionData.ShaderDescriptorSets.size()); + SE_CORE_ASSERT(m_ReflectionData.ShaderDescriptorSets[set]); + if (m_ReflectionData.ShaderDescriptorSets.at(set).WriteDescriptorSets.find(name) == m_ReflectionData.ShaderDescriptorSets.at(set).WriteDescriptorSets.end()) + { + SE_CORE_WARN_TAG("Renderer", "Shader {0} does not contain requested descriptor set {1}", m_Name, name); + return nullptr; + } + return &m_ReflectionData.ShaderDescriptorSets.at(set).WriteDescriptorSets.at(name); + } + +#endif + + const std::unordered_map& VulkanShader::GetResources() const + { + return m_ReflectionData.Resources; + } + + void VulkanShader::AddShaderReloadedCallback(const ShaderReloadedCallback& callback) + { + } + + bool VulkanShader::TryReadReflectionData(StreamReader* serializer) + { + uint32_t shaderDescriptorSetCount; + serializer->ReadRaw(shaderDescriptorSetCount); + + for (uint32_t i = 0; i < shaderDescriptorSetCount; i++) + { + auto& descriptorSet = m_ReflectionData.ShaderDescriptorSets.emplace_back(); + serializer->ReadMap(descriptorSet.UniformBuffers); + serializer->ReadMap(descriptorSet.StorageBuffers); + serializer->ReadMap(descriptorSet.ImageSamplers); + serializer->ReadMap(descriptorSet.StorageImages); + serializer->ReadMap(descriptorSet.SeparateTextures); + serializer->ReadMap(descriptorSet.SeparateSamplers); + serializer->ReadMap(descriptorSet.InputDeclarations); + } + + serializer->ReadMap(m_ReflectionData.Resources); + serializer->ReadMap(m_ReflectionData.ConstantBuffers); + serializer->ReadArray(m_ReflectionData.PushConstantRanges); + + return true; + } + + void VulkanShader::SerializeReflectionData(StreamWriter* serializer) + { + serializer->WriteRaw((uint32_t)m_ReflectionData.ShaderDescriptorSets.size()); + for (const auto& descriptorSet : m_ReflectionData.ShaderDescriptorSets) + { + serializer->WriteMap(descriptorSet.UniformBuffers); + serializer->WriteMap(descriptorSet.StorageBuffers); + serializer->WriteMap(descriptorSet.ImageSamplers); + serializer->WriteMap(descriptorSet.StorageImages); + serializer->WriteMap(descriptorSet.SeparateTextures); + serializer->WriteMap(descriptorSet.SeparateSamplers); + serializer->WriteMap(descriptorSet.InputDeclarations); + } + + serializer->WriteMap(m_ReflectionData.Resources); + serializer->WriteMap(m_ReflectionData.ConstantBuffers); + serializer->WriteArray(m_ReflectionData.PushConstantRanges); + } + + void VulkanShader::SetReflectionData(const ReflectionData& reflectionData) + { + m_ReflectionData = reflectionData; + } + + nvrhi::ShaderHandle VulkanShader::GetHandle(nvrhi::ShaderType type) const + { + SE_CORE_VERIFY(m_ShaderHandles.contains(type)); + return m_ShaderHandles.at(type); + } + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShader.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShader.h new file mode 100644 index 00000000..27b1bdcf --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShader.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include + +#include "StarEngine/Renderer/Shader.h" +#include "VulkanShaderResource.h" + +#include "nvrhi/nvrhi.h" + +#include "VulkanMemoryAllocator/vk_mem_alloc.h" + +namespace StarEngine { + + class VulkanShader : public Shader + { + public: + struct ReflectionData + { + std::vector ShaderDescriptorSets; + std::unordered_map Resources; + std::unordered_map ConstantBuffers; + std::vector PushConstantRanges; + }; + public: + VulkanShader() = default; + VulkanShader(const std::string& path, bool forceCompile, bool disableOptimization); + virtual ~VulkanShader(); + void Release(); + + void Reload(bool forceCompile = false) override; + void RT_Reload(bool forceCompile) override; + + virtual size_t GetHash() const override; + void SetMacro(const std::string& name, const std::string& value) override {} + + virtual const std::string& GetName() const override { return m_Name; } + virtual const std::unordered_map& GetShaderBuffers() const override { return m_ReflectionData.ConstantBuffers; } + virtual const std::unordered_map& GetResources() const override; + virtual void AddShaderReloadedCallback(const ShaderReloadedCallback& callback) override; + + bool TryReadReflectionData(StreamReader* serializer); + + void SerializeReflectionData(StreamWriter* serializer); + + void SetReflectionData(const ReflectionData& reflectionData); + + virtual nvrhi::ShaderHandle GetHandle() const override { return m_ShaderHandles.begin()->second; } + virtual nvrhi::ShaderHandle GetHandle(nvrhi::ShaderType type) const override; + virtual const std::map& GetHandles() const override { return m_ShaderHandles; } + + // Vulkan-specific + const std::vector& GetPipelineShaderStageCreateInfos() const { return m_PipelineShaderStageCreateInfos; } + + VkDescriptorSet GetDescriptorSet() { return m_DescriptorSet; } + + nvrhi::BindingLayoutHandle GetDescriptorSetLayout(uint32_t set = 0) { return m_DescriptorSetLayouts[set]; } + const nvrhi::BindingLayoutVector& GetAllDescriptorSetLayouts() const { return m_DescriptorSetLayouts; } + + ShaderResource::UniformBuffer& GetUniformBuffer(const uint32_t binding = 0, const uint32_t set = 0) { SE_CORE_ASSERT(m_ReflectionData.ShaderDescriptorSets.at(set).UniformBuffers.size() > binding); return m_ReflectionData.ShaderDescriptorSets.at(set).UniformBuffers.at(binding); } + uint32_t GetUniformBufferCount(const uint32_t set = 0) + { + if (m_ReflectionData.ShaderDescriptorSets.size() < set) + return 0; + + return (uint32_t)m_ReflectionData.ShaderDescriptorSets[set].UniformBuffers.size(); + } + + const std::vector& GetShaderDescriptorSets() const { return m_ReflectionData.ShaderDescriptorSets; } + bool HasDescriptorSet(uint32_t set) const { return m_TypeCounts.find(set) != m_TypeCounts.end(); } + + const std::vector& GetPushConstantRanges() const { return m_ReflectionData.PushConstantRanges; } + + struct ShaderMaterialDescriptorSet + { + VkDescriptorPool Pool = nullptr; + std::vector DescriptorSets; + }; + +#if OLD + ShaderMaterialDescriptorSet AllocateDescriptorSet(uint32_t set = 0); + ShaderMaterialDescriptorSet CreateDescriptorSets(uint32_t set = 0); + ShaderMaterialDescriptorSet CreateDescriptorSets(uint32_t set, uint32_t numberOfSets); +#endif + + const VkWriteDescriptorSet* GetDescriptorSet(const std::string& name, uint32_t set = 0) const; + private: + void LoadAndCreateShaders(const std::map>& shaderData); + void CreateDescriptors(); + private: + std::map m_ShaderHandles; + + std::vector m_PipelineShaderStageCreateInfos; + + std::filesystem::path m_AssetPath; + std::string m_Name; + bool m_DisableOptimization = false; + + std::map> m_ShaderData; + ReflectionData m_ReflectionData; + + nvrhi::BindingLayoutVector m_DescriptorSetLayouts; + VkDescriptorSet m_DescriptorSet; + + std::unordered_map> m_TypeCounts; + private: + friend class ShaderCache; + friend class ShaderPack; + friend class VulkanShaderCompiler; + }; + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderResource.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderResource.h new file mode 100644 index 00000000..4dbdc65a --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderResource.h @@ -0,0 +1,181 @@ +#pragma once + +#include "vulkan/vulkan.h" +#include "VulkanAllocator.h" + +#include "StarEngine/Serialization/StreamReader.h" +#include "StarEngine/Serialization/StreamWriter.h" + +#include "nvrhi/nvrhi.h" + +#include + +namespace StarEngine { + + enum class RenderResourceType : uint16_t + { + None = 0, + UniformBuffer, + UniformBufferSet, + StorageBuffer, + StorageBufferSet, + Texture2D, + TextureCube, + Image2D, + Sampler, + }; + + enum class RenderInputType : uint16_t + { + None = 0, + UniformBuffer, + StorageBuffer, + ImageSampler, + ImageSampler1D, + ImageSampler2D, + ImageSampler3D, // NOTE(Yan): 3D vs Cube? + StorageImage1D, + StorageImage2D, + StorageImage3D + }; + + struct RenderInputDeclaration + { + RenderInputType Type = RenderInputType::None; + uint32_t Set = 0; + uint32_t Binding = 0; + uint32_t Count = 0; + std::string Name; + + static void Serialize(StreamWriter* serializer, const RenderInputDeclaration& instance) + { + serializer->WriteRaw(instance.Type); + serializer->WriteRaw(instance.Set); + serializer->WriteRaw(instance.Binding); + serializer->WriteRaw(instance.Count); + serializer->WriteString(instance.Name); + } + + static void Deserialize(StreamReader* deserializer, RenderInputDeclaration& instance) + { + deserializer->ReadRaw(instance.Type); + deserializer->ReadRaw(instance.Set); + deserializer->ReadRaw(instance.Binding); + deserializer->ReadRaw(instance.Count); + deserializer->ReadString(instance.Name); + } + }; + + namespace ShaderResource { + + struct UniformBuffer + { + VkDescriptorBufferInfo Descriptor; + uint32_t Size = 0; + uint32_t BindingPoint = 0; + std::string Name; + nvrhi::ShaderType ShaderStage = nvrhi::ShaderType::None; + + static void Serialize(StreamWriter* serializer, const UniformBuffer& instance) + { + serializer->WriteRaw(instance.Descriptor); + serializer->WriteRaw(instance.Size); + serializer->WriteRaw(instance.BindingPoint); + serializer->WriteString(instance.Name); + serializer->WriteRaw(instance.ShaderStage); + } + + static void Deserialize(StreamReader* deserializer, UniformBuffer& instance) + { + deserializer->ReadRaw(instance.Descriptor); + deserializer->ReadRaw(instance.Size); + deserializer->ReadRaw(instance.BindingPoint); + deserializer->ReadString(instance.Name); + deserializer->ReadRaw(instance.ShaderStage); + } + }; + + struct StorageBuffer + { + VmaAllocation MemoryAlloc = nullptr; + VkDescriptorBufferInfo Descriptor; + uint32_t Size = 0; + uint32_t BindingPoint = 0; + std::string Name; + nvrhi::ShaderType ShaderStage = nvrhi::ShaderType::None; + + static void Serialize(StreamWriter* serializer, const StorageBuffer& instance) + { + serializer->WriteRaw(instance.Descriptor); + serializer->WriteRaw(instance.Size); + serializer->WriteRaw(instance.BindingPoint); + serializer->WriteString(instance.Name); + serializer->WriteRaw(instance.ShaderStage); + } + + static void Deserialize(StreamReader* deserializer, StorageBuffer& instance) + { + deserializer->ReadRaw(instance.Descriptor); + deserializer->ReadRaw(instance.Size); + deserializer->ReadRaw(instance.BindingPoint); + deserializer->ReadString(instance.Name); + deserializer->ReadRaw(instance.ShaderStage); + } + }; + + struct ImageSampler + { + uint32_t BindingPoint = 0; + uint32_t DescriptorSet = 0; + uint32_t Dimension = 0; + uint32_t ArraySize = 0; + std::string Name; + nvrhi::ShaderType ShaderStage = nvrhi::ShaderType::None; + + static void Serialize(StreamWriter* serializer, const ImageSampler& instance) + { + serializer->WriteRaw(instance.BindingPoint); + serializer->WriteRaw(instance.DescriptorSet); + serializer->WriteRaw(instance.Dimension); + serializer->WriteRaw(instance.ArraySize); + serializer->WriteString(instance.Name); + serializer->WriteRaw(instance.ShaderStage); + } + + static void Deserialize(StreamReader* deserializer, ImageSampler& instance) + { + deserializer->ReadRaw(instance.BindingPoint); + deserializer->ReadRaw(instance.DescriptorSet); + deserializer->ReadRaw(instance.Dimension); + deserializer->ReadRaw(instance.ArraySize); + deserializer->ReadString(instance.Name); + deserializer->ReadRaw(instance.ShaderStage); + } + }; + + struct PushConstantRange + { + nvrhi::ShaderType ShaderStage = nvrhi::ShaderType::None; + uint32_t Offset = 0; + uint32_t Size = 0; + + static void Serialize(StreamWriter* writer, const PushConstantRange& range) { writer->WriteRaw(range); } + static void Deserialize(StreamReader* reader, PushConstantRange& range) { reader->ReadRaw(range); } + }; + + struct ShaderDescriptorSet + { + std::unordered_map UniformBuffers; + std::unordered_map StorageBuffers; + std::unordered_map ImageSamplers; + std::unordered_map StorageImages; + std::unordered_map SeparateTextures; // Not really an image sampler. + std::unordered_map SeparateSamplers; + + std::unordered_map InputDeclarations; + + operator bool() const { return !(StorageBuffers.empty() && UniformBuffers.empty() && ImageSamplers.empty() && StorageImages.empty() && SeparateTextures.empty() && SeparateSamplers.empty()); } + }; + + } +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderUtils.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderUtils.cpp new file mode 100644 index 00000000..305e4361 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderUtils.cpp @@ -0,0 +1,10 @@ +#include "sepch.h" +#include "VulkanShaderUtils.h" + + +namespace StarEngine { + + + + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderUtils.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderUtils.h new file mode 100644 index 00000000..7881b38a --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanShaderUtils.h @@ -0,0 +1,134 @@ +#pragma once + +#include "StarEngine/Renderer/Shader.h" + +#include "nvrhi/nvrhi.h" + +#include + +#include + + +namespace StarEngine { namespace ShaderUtils { + +#if 0 + inline static std::string_view VKStageToShaderMacro(const VkShaderStageFlagBits stage) + { + if (stage == VK_SHADER_STAGE_VERTEX_BIT) return "__VERTEX_STAGE__"; + if (stage == VK_SHADER_STAGE_FRAGMENT_BIT) return "__FRAGMENT_STAGE__"; + if (stage == VK_SHADER_STAGE_COMPUTE_BIT) return "__COMPUTE_STAGE__"; + SE_CORE_VERIFY(false, "Unknown shader stage."); + return ""; + } + + inline static std::string_view StageToShaderMacro(const std::string_view stage) + { + if (stage == "vert") return "__VERTEX_STAGE__"; + if (stage == "frag") return "__FRAGMENT_STAGE__"; + if (stage == "comp") return "__COMPUTE_STAGE__"; + SE_CORE_VERIFY(false, "Unknown shader stage."); + return ""; + } + + inline static VkShaderStageFlagBits StageToVKShaderStage(const std::string_view stage) + { + if (stage == "vert") return VK_SHADER_STAGE_VERTEX_BIT; + if (stage == "frag") return VK_SHADER_STAGE_FRAGMENT_BIT; + if (stage == "comp") return VK_SHADER_STAGE_COMPUTE_BIT; + SE_CORE_VERIFY(false, "Unknown shader stage."); + return VK_SHADER_STAGE_ALL; + } +#endif + inline static nvrhi::ShaderType PreprocessorStageToShaderStage(const std::string_view stage) + { + if (stage == "vert") return nvrhi::ShaderType::Vertex; + if (stage == "frag") return nvrhi::ShaderType::Pixel; + if (stage == "comp") return nvrhi::ShaderType::Compute; + SE_CORE_VERIFY(false, "Unknown shader stage."); + return nvrhi::ShaderType::None; + } + + inline static std::string_view ShaderStageToShaderMacro(const nvrhi::ShaderType stage) + { + if (stage == nvrhi::ShaderType::Vertex) return "__VERTEX_STAGE__"; + if (stage == nvrhi::ShaderType::Pixel) return "__FRAGMENT_STAGE__"; + if (stage == nvrhi::ShaderType::Compute) return "__COMPUTE_STAGE__"; + SE_CORE_VERIFY(false, "Unknown shader stage."); + return ""; + } + + inline static SourceLang ShaderLangFromExtension(const std::string_view type) + { + if (type == ".glsl") return SourceLang::GLSL; + if (type == ".hlsl") return SourceLang::HLSL; + + SE_CORE_ASSERT(false); + + return SourceLang::NONE; + } + + inline static shaderc_shader_kind ShaderStageToShaderC(const nvrhi::ShaderType stage) + { + switch (stage) + { + case nvrhi::ShaderType::Vertex: return shaderc_vertex_shader; + case nvrhi::ShaderType::Pixel: return shaderc_fragment_shader; + case nvrhi::ShaderType::Compute: return shaderc_compute_shader; + } + SE_CORE_ASSERT(false); + return {}; + } + + inline static const char* ShaderStageCachedFileExtension(const nvrhi::ShaderType stage, bool debug) + { + if (debug) + { + switch (stage) + { + case nvrhi::ShaderType::Vertex: return ".cached_vulkan_debug.vert"; + case nvrhi::ShaderType::Pixel: return ".cached_vulkan_debug.frag"; + case nvrhi::ShaderType::Compute: return ".cached_vulkan_debug.comp"; + } + } + else + { + switch (stage) + { + case nvrhi::ShaderType::Vertex: return ".cached_vulkan.vert"; + case nvrhi::ShaderType::Pixel: return ".cached_vulkan.frag"; + case nvrhi::ShaderType::Compute: return ".cached_vulkan.comp"; + } + } + SE_CORE_ASSERT(false); + return ""; + } + +#ifdef SE_PLATFORM_WINDOWS + inline static const wchar_t* HLSLShaderProfile(const nvrhi::ShaderType stage) + { + switch (stage) + { + case nvrhi::ShaderType::Vertex: return L"vs_6_0"; + case nvrhi::ShaderType::Pixel: return L"ps_6_0"; + case nvrhi::ShaderType::Compute: return L"cs_6_0"; + } + SE_CORE_ASSERT(false); + return L""; + } +#else + inline static const char* HLSLShaderProfile(const nvrhi::ShaderType stage) + { + switch (stage) + { + case nvrhi::ShaderType::Vertex: return "vs_6_0"; + case nvrhi::ShaderType::Pixel: return "ps_6_0"; + case nvrhi::ShaderType::Compute: return "cs_6_0"; + } + SE_CORE_ASSERT(false); + return ""; + } +#endif + } + + +} diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanSwapChain.cpp b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanSwapChain.cpp new file mode 100644 index 00000000..5aa84738 --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanSwapChain.cpp @@ -0,0 +1,357 @@ +#include "sepch.h" +#include "VulkanSwapChain.h" + +#include "StarEngine/Core/Application.h" +#include "StarEngine/Debug/Profiler.h" + +#include + +#include "VulkanDeviceManager.h" + +namespace StarEngine { + + namespace Utils { + template + static std::vector SetToVector(const std::unordered_set& set) + { + std::vector ret; + for (const auto& s : set) + { + ret.push_back(s); + } + + return ret; + } + } + + VulkanSwapChain::VulkanSwapChain(vk::SurfaceKHR surface) + : m_Surface(surface) + { + } + + bool VulkanSwapChain::Create(uint32_t width, uint32_t height) + { + m_Width = width; + m_Height = height; + + Destroy(); + + const auto& deviceParams = Application::GetGraphicsDeviceManager()->GetDeviceParams(); + auto device = Application::GetGraphicsDevice(); + + VulkanDeviceManager* vulkanDeviceManager = (VulkanDeviceManager*)Application::GetGraphicsDeviceManager(); + + m_SwapChainFormat = { + vk::Format(nvrhi::vulkan::convertFormat(deviceParams.swapChainFormat)), + vk::ColorSpaceKHR::eSrgbNonlinear + }; + + vk::Extent2D extent = vk::Extent2D(m_Width, m_Height); + + std::unordered_set uniqueQueues = { + uint32_t(vulkanDeviceManager->m_QueueFamilyIndices.Graphics), + uint32_t(vulkanDeviceManager->m_QueueFamilyIndices.Present) }; + + std::vector queues = Utils::SetToVector(uniqueQueues); + + const bool enableSwapChainSharing = queues.size() > 1; + + auto desc = vk::SwapchainCreateInfoKHR() + .setSurface(m_Surface) + .setMinImageCount(deviceParams.swapChainBufferCount) + .setImageFormat(m_SwapChainFormat.format) + .setImageColorSpace(m_SwapChainFormat.colorSpace) + .setImageExtent(extent) + .setImageArrayLayers(1) + .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled) + .setImageSharingMode(enableSwapChainSharing ? vk::SharingMode::eConcurrent : vk::SharingMode::eExclusive) + .setFlags(vulkanDeviceManager->m_SwapChainMutableFormatSupported ? vk::SwapchainCreateFlagBitsKHR::eMutableFormat : vk::SwapchainCreateFlagBitsKHR(0)) + .setQueueFamilyIndexCount(enableSwapChainSharing ? uint32_t(queues.size()) : 0) + .setPQueueFamilyIndices(enableSwapChainSharing ? queues.data() : nullptr) + .setPreTransform(vk::SurfaceTransformFlagBitsKHR::eIdentity) + .setCompositeAlpha(vk::CompositeAlphaFlagBitsKHR::eOpaque) + .setPresentMode(deviceParams.vsyncEnabled ? vk::PresentModeKHR::eFifo : vk::PresentModeKHR::eImmediate) + .setClipped(true) + .setOldSwapchain(nullptr); + + std::vector imageFormats = { m_SwapChainFormat.format }; + switch (m_SwapChainFormat.format) + { + case vk::Format::eR8G8B8A8Unorm: + imageFormats.push_back(vk::Format::eR8G8B8A8Srgb); + break; + case vk::Format::eR8G8B8A8Srgb: + imageFormats.push_back(vk::Format::eR8G8B8A8Unorm); + break; + case vk::Format::eB8G8R8A8Unorm: + imageFormats.push_back(vk::Format::eB8G8R8A8Srgb); + break; + case vk::Format::eB8G8R8A8Srgb: + imageFormats.push_back(vk::Format::eB8G8R8A8Unorm); + break; + } + + auto imageFormatListCreateInfo = vk::ImageFormatListCreateInfo() + .setViewFormats(imageFormats); + + if (vulkanDeviceManager->m_SwapChainMutableFormatSupported) + desc.pNext = &imageFormatListCreateInfo; + + const vk::Result res = vulkanDeviceManager->m_VulkanDevice.createSwapchainKHR(&desc, nullptr, &m_SwapChain); + if (res != vk::Result::eSuccess) + { + SE_CORE_ERROR("Failed to create a Vulkan swap chain, error code = {}", nvrhi::vulkan::resultToString(VkResult(res))); + return false; + } + + // retrieve swap chain images + auto images = vulkanDeviceManager->m_VulkanDevice.getSwapchainImagesKHR(m_SwapChain); + for (auto image : images) + { + SwapChainImage sci; + sci.image = image; + + nvrhi::TextureDesc textureDesc; + textureDesc.width = m_Width; + textureDesc.height = m_Height; + textureDesc.format = deviceParams.swapChainFormat; + textureDesc.debugName = "Swap chain image"; + textureDesc.initialState = nvrhi::ResourceStates::Present; + textureDesc.keepInitialState = true; + textureDesc.isRenderTarget = true; + + sci.rhiHandle = device->createHandleForNativeTexture(nvrhi::ObjectTypes::VK_Image, nvrhi::Object(sci.image), textureDesc); + m_SwapChainImages.push_back(sci); + } + + m_SwapChainIndex = 0; + + for (uint32_t i = 0; i < 3; ++i) + { + m_PresentSemaphores[i] = vulkanDeviceManager->m_VulkanDevice.createSemaphore(vk::SemaphoreCreateInfo()); + m_AcquireSemaphores[i] = vulkanDeviceManager->m_VulkanDevice.createSemaphore(vk::SemaphoreCreateInfo()); + } + + BackBufferResized(); + } + + void VulkanSwapChain::Destroy() + { + VulkanDeviceManager* vulkanDeviceManager = (VulkanDeviceManager*)Application::GetGraphicsDeviceManager(); + if (vulkanDeviceManager->m_VulkanDevice) + { + vulkanDeviceManager->m_VulkanDevice.waitIdle(); + } + + if (m_SwapChain) + { + vulkanDeviceManager->m_VulkanDevice.destroySwapchainKHR(m_SwapChain); + m_SwapChain = nullptr; + } + + m_SwapChainImages.clear(); + + for (auto& semaphore : m_PresentSemaphores) + { + if (semaphore) + { + vulkanDeviceManager->m_VulkanDevice.destroySemaphore(semaphore); + semaphore = vk::Semaphore(); + } + } + + for (auto& semaphore : m_AcquireSemaphores) + { + if (semaphore) + { + vulkanDeviceManager->m_VulkanDevice.destroySemaphore(semaphore); + semaphore = vk::Semaphore(); + } + } + + } + + void VulkanSwapChain::OnResize(uint32_t width, uint32_t height) + { + m_Width = width; + m_Height = height; + BackBufferResizing(); + Resize(); + } + + void VulkanSwapChain::Resize() + { + Destroy(); + Create(m_Width, m_Height); + } + + bool VulkanSwapChain::BeginFrame() + { + SE_PROFILE_FUNCTION("VulkanSwapChain::BeginFrame"); + + auto device = (nvrhi::vulkan::IDevice*)Application::GetGraphicsDevice().Get(); + VulkanDeviceManager* vulkanDeviceManager = (VulkanDeviceManager*)Application::GetGraphicsDeviceManager(); + + const auto& semaphore = m_AcquireSemaphores[m_AcquireSemaphoreIndex]; + + vk::Result res; + + int const maxAttempts = 3; + for (int attempt = 0; attempt < maxAttempts; ++attempt) + { + res = vulkanDeviceManager->m_VulkanDevice.acquireNextImageKHR( + m_SwapChain, + std::numeric_limits::max(), // timeout + semaphore, + vk::Fence(), + &m_SwapChainIndex); + + m_AcquiredSemaphore = semaphore; + + if (res == vk::Result::eErrorOutOfDateKHR && attempt < maxAttempts) + { + BackBufferResizing(); + auto surfaceCaps = vulkanDeviceManager->m_VulkanPhysicalDevice.getSurfaceCapabilitiesKHR(m_Surface); + + m_Width = surfaceCaps.currentExtent.width; + m_Height = surfaceCaps.currentExtent.height; + + Resize(); + BackBufferResized(); + } + else + { + break; + } + } + + m_AcquireSemaphoreIndex = (m_AcquireSemaphoreIndex + 1) % m_AcquireSemaphores.size(); + + return res == vk::Result::eSuccess; + } + + void VulkanSwapChain::Present() + { + SE_PROFILE_FUNCTION("VulkanSwapChain::Present"); + + VulkanDeviceManager* vulkanDeviceManager = (VulkanDeviceManager*)Application::GetGraphicsDeviceManager(); + auto device = (nvrhi::vulkan::IDevice*)Application::GetGraphicsDevice().Get(); + const auto& semaphore = m_PresentSemaphores[m_PresentSemaphoreIndex]; + + RenderCommandBuffer::LockQueue(); + device->queueSignalSemaphore(nvrhi::CommandQueue::Graphics, semaphore, 0); + device->executeCommandLists(nullptr, 0); + + { + SE_PROFILE_FUNCTION("PresentKHR"); + vk::PresentInfoKHR info = vk::PresentInfoKHR() + .setWaitSemaphoreCount(1) + .setPWaitSemaphores(&semaphore) + .setSwapchainCount(1) + .setPSwapchains(&m_SwapChain) + .setPImageIndices(&m_SwapChainIndex); + + const vk::Result res = vulkanDeviceManager->m_PresentQueue.presentKHR(&info); + SE_CORE_VERIFY(res == vk::Result::eSuccess || res == vk::Result::eErrorOutOfDateKHR); + + m_PresentSemaphoreIndex = (m_PresentSemaphoreIndex + 1) % m_PresentSemaphores.size(); + } + + RenderCommandBuffer::UnlockQueue(); + +#ifndef _WIN32 + if (deviceParams.vsyncEnabled) + { + m_PresentQueue.waitIdle(); + } +#endif + + { + SE_PROFILE_SCOPE("WaitEventQuery"); + + while (m_FramesInFlight.size() >= vulkanDeviceManager->m_DeviceParams.maxFramesInFlight) + { + auto query = m_FramesInFlight.front(); + m_FramesInFlight.pop(); + + device->waitEventQuery(query); + + m_QueryPool.push_back(query); + } + } + + { + SE_PROFILE_SCOPE("OtherEventQuery"); + nvrhi::EventQueryHandle query; + if (!m_QueryPool.empty()) + { + query = m_QueryPool.back(); + m_QueryPool.pop_back(); + } + else + { + query = device->createEventQuery(); + } + + device->resetEventQuery(query); + device->setEventQuery(query, nvrhi::CommandQueue::Graphics); + m_FramesInFlight.push(query); + } + } + + void VulkanSwapChain::BackBufferResizing() + { + m_SwapChainFramebuffers.clear(); + } + + void VulkanSwapChain::BackBufferResized() + { + auto device = Application::GetGraphicsDevice(); + uint32_t backBufferCount = GetBackBufferCount(); + m_SwapChainFramebuffers.resize(backBufferCount); + for (uint32_t index = 0; index < backBufferCount; index++) + { + m_SwapChainFramebuffers[index] = device->createFramebuffer( + nvrhi::FramebufferDesc().addColorAttachment(GetBackBuffer(index))); + } + } + + nvrhi::ITexture* VulkanSwapChain::GetCurrentBackBuffer() + { + return m_SwapChainImages[m_SwapChainIndex].rhiHandle; + } + + nvrhi::ITexture* VulkanSwapChain::GetBackBuffer(uint32_t index) + { + if (index < m_SwapChainImages.size()) + return m_SwapChainImages[index].rhiHandle; + return nullptr; + } + + uint32_t VulkanSwapChain::GetCurrentBackBufferIndex() + { + return m_SwapChainIndex; + } + + uint32_t VulkanSwapChain::GetBackBufferCount() + { + return uint32_t(m_SwapChainImages.size()); + } + + nvrhi::IFramebuffer* VulkanSwapChain::GetCurrentFramebuffer() + { + return GetFramebuffer(GetCurrentBackBufferIndex()); + } + + nvrhi::IFramebuffer* VulkanSwapChain::GetFramebuffer(uint32_t index) + { + if (index < m_SwapChainFramebuffers.size()) + return m_SwapChainFramebuffers[index]; + + return nullptr; + } + + +} + + diff --git a/StarEngine/src/StarEngine/Platform/Vulkan/VulkanSwapChain.h b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanSwapChain.h new file mode 100644 index 00000000..9a6f47dc --- /dev/null +++ b/StarEngine/src/StarEngine/Platform/Vulkan/VulkanSwapChain.h @@ -0,0 +1,71 @@ +#pragma once + +#include "StarEngine/Core/Base.h" + +#include "Vulkan.h" +#include "VulkanDevice.h" + +#include "nvrhi/nvrhi.h" + +#include + +#include +#include + +struct GLFWwindow; + +namespace StarEngine { + + class VulkanSwapChain + { + public: + bool Create(uint32_t width, uint32_t height); + void Destroy(); + + void OnResize(uint32_t width, uint32_t height); + void Resize(); + + bool BeginFrame(); + void Present(); + + nvrhi::ITexture* GetCurrentBackBuffer(); + nvrhi::ITexture* GetBackBuffer(uint32_t index); + uint32_t GetCurrentBackBufferIndex(); + uint32_t GetBackBufferCount(); + vk::Semaphore GetAcquiredImageSemaphore() const { return m_AcquiredSemaphore; } + + nvrhi::IFramebuffer* GetCurrentFramebuffer(); + nvrhi::IFramebuffer* GetFramebuffer(uint32_t index); + + void BackBufferResizing(); + void BackBufferResized(); + public: + VulkanSwapChain(vk::SurfaceKHR surface); + private: + vk::SurfaceKHR m_Surface = nullptr; + uint32_t m_Width = 0, m_Height = 0; + + std::array m_AcquireSemaphores; + std::array m_PresentSemaphores; + vk::Semaphore m_AcquiredSemaphore; + + std::vector m_SwapChainFramebuffers; + + vk::SurfaceFormatKHR m_SwapChainFormat; + vk::SwapchainKHR m_SwapChain; + + uint32_t m_AcquireSemaphoreIndex = 0; + uint32_t m_PresentSemaphoreIndex = 0; + + struct SwapChainImage + { + vk::Image image; + nvrhi::TextureHandle rhiHandle; + }; + std::vector m_SwapChainImages; + uint32_t m_SwapChainIndex = uint32_t(-1); + + std::queue m_FramesInFlight; + std::vector m_QueryPool; + }; +} diff --git a/StarEngine/src/StarEngine/Project/Project.cpp b/StarEngine/src/StarEngine/Project/Project.cpp index fe373099..5790ad7a 100644 --- a/StarEngine/src/StarEngine/Project/Project.cpp +++ b/StarEngine/src/StarEngine/Project/Project.cpp @@ -1,65 +1,98 @@ #include "sepch.h" #include "Project.h" -#include "ProjectSerializer.h" -#include"StarEngine/Audio/AudioEngine.h" +#include "StarEngine/Asset/AssetManager.h" +#include "StarEngine/Audio/AudioEngine.h" +#include "StarEngine/Audio/AudioEvents/AudioCommandRegistry.h" + +#include "StarEngine/Physics/PhysicsSystem.h" + +#include "StarEngine/Scripting/ScriptEngine.h" namespace StarEngine { - std::filesystem::path Project::GetAssetAbsolutePath(const std::filesystem::path& path) + Project::Project() { - return GetAssetDirectory() / path; + m_AudioCommands = Ref::Create(); } - Ref Project::New() + Project::~Project() { - s_ActiveProject = CreateRef(); - return s_ActiveProject; } - Ref Project::Load(const std::filesystem::path& path) + void Project::ReloadScriptEngine() { - Ref project = CreateRef(); + auto& scriptEngine = ScriptEngine::GetMutable(); + scriptEngine.Shutdown(); + scriptEngine.Initialize(this); + scriptEngine.LoadProjectAssembly(); + } - ProjectSerializer serializer(project); - if (serializer.Deserialize(path)) + void Project::SetActive(Ref project) + { + if (s_ActiveProject) { - if (AudioEngine::HasInitializedEngine()) - { - AudioEngine::Shutdown(); - AudioEngine::SetInitalizedEngine(false); - } + s_AssetManager->Shutdown(); + s_AssetManager = nullptr; + ScriptEngine::GetMutable().Shutdown(); + PhysicsSystem::Shutdown(); + AudioCommandRegistry::Shutdown(); + } - project->m_ProjectDirectory = path.parent_path(); - s_ActiveProject = project; + s_ActiveProject = project; + if (s_ActiveProject) + { + PhysicsAPI::SetCurrentAPI(s_ActiveProject->GetConfig().CurrentPhysicsAPI); - Ref editorAssetManager = std::make_shared(); - s_ActiveProject->m_AssetManager = editorAssetManager; - editorAssetManager->DeserializeAssetRegistry(); + s_AssetManager = Ref::Create(); + PhysicsSystem::Init(); - if (!AudioEngine::HasInitializedEngine()) - { - AudioEngine::Init(); - AudioEngine::SetInitalizedEngine(true); - } + MiniAudioEngine::Get().OnProjectLoaded(); + // AudioCommandsRegistry must be deserialized after AssetManager, + // otheriwse all of the command action targets are going to be invalid and cleared! + WeakRef audioCommands = s_ActiveProject->m_AudioCommands; + AudioCommandRegistry::Init(audioCommands); - return s_ActiveProject; + ScriptEngine::GetMutable().Initialize(project); } - - return nullptr; } - bool Project::SaveActive(const std::filesystem::path& path) + void Project::SetActiveRuntime(Ref project, Ref assetPack) { - ProjectSerializer serializer(s_ActiveProject); - if (serializer.Serialize(path)) + if (s_ActiveProject) { - s_ActiveProject->m_ProjectDirectory = path.parent_path(); - return true; + s_AssetManager = nullptr; + ScriptEngine::GetMutable().Shutdown(); + PhysicsSystem::Shutdown(); + AudioCommandRegistry::Shutdown(); } - return false; + s_ActiveProject = project; + if (s_ActiveProject) + { + s_AssetManager = Ref::Create(); + Project::GetRuntimeAssetManager()->SetAssetPack(assetPack); + + PhysicsSystem::Init(); + + // AudioCommandsRegistry must be deserialized after AssetManager, + // otherwise all of the command action targets are going to be invalid and cleared! + WeakRef audioCommands = s_ActiveProject->m_AudioCommands; + //if (!runtime) + MiniAudioEngine::Get().OnProjectLoaded(); + AudioCommandRegistry::Init(audioCommands); + ScriptEngine::GetMutable().Initialize(project); + } + } + + void Project::OnSerialized() + { + m_AudioCommands->WriteRegistryToFile(std::filesystem::path(m_Config.ProjectDirectory) / m_Config.AudioCommandsRegistryPath); + } + + void Project::OnDeserialized() + { } } diff --git a/StarEngine/src/StarEngine/Project/Project.h b/StarEngine/src/StarEngine/Project/Project.h index 40892961..102edc51 100644 --- a/StarEngine/src/StarEngine/Project/Project.h +++ b/StarEngine/src/StarEngine/Project/Project.h @@ -1,41 +1,86 @@ #pragma once -#include -#include +#include "StarEngine/Asset/AssetManager/EditorAssetManager.h" +#include "StarEngine/Asset/AssetManager/RuntimeAssetManager.h" +#include "StarEngine/Core/Log.h" +#include "StarEngine/Core/Ref.h" +#include "StarEngine/Physics/PhysicsAPI.h" -#include "StarEngine/Core/Base.h" +#include +#include -#include "StarEngine/Asset/RuntimeAssetManager.h" -#include "StarEngine/Asset/EditorAssetManager.h" namespace StarEngine { + class AudioCommandRegistry; + struct ProjectConfig { - std::string Name = "Untitled"; + std::string Name; + + std::string AssetDirectory = "Assets"; + std::string AssetRegistryPath = "Assets/AssetRegistry.ser"; + + std::string AudioCommandsRegistryPath = "Assets/AudioCommandsRegistry.ser"; + + std::string MeshPath = "Assets/Meshes"; + std::string MeshSourcePath = "Assets/Meshes/Source"; + + std::string AnimationPath; - AssetHandle StartScene; + std::string ScriptModulePath = "Assets/Scripts/Binaries"; + std::string DefaultNamespace; - std::filesystem::path AssetDirectory; - std::filesystem::path AssetRegistryPath; // Relative to AssetDirectory - std::filesystem::path ScriptModulePath; + std::string StartScene; + + bool AutomaticallyReloadAssembly; + + bool EnableAutoSave = false; + int AutoSaveIntervalSeconds = 300; + + PhysicsAPIType CurrentPhysicsAPI = PhysicsAPIType::Jolt; + + // Not serialized + std::string ProjectFileName; + std::string ProjectDirectory; + + // Runtime only + AssetHandle StartSceneHandle; }; - class Project + class Project : public RefCounted { public: - const std::filesystem::path& GetProjectDirectory() { return m_ProjectDirectory; } - std::filesystem::path GetAssetDirectory() { return GetProjectDirectory() / s_ActiveProject->m_Config.AssetDirectory; } - std::filesystem::path GetAssetRegistryPath() { return GetAssetDirectory() / s_ActiveProject->m_Config.AssetRegistryPath; } - // TODO: move to asset manager when we have one - std::filesystem::path GetAssetFileSystemPath(const std::filesystem::path& path) { return GetAssetDirectory() / path; } + Project(); + ~Project(); + + const ProjectConfig& GetConfig() const { return m_Config; } - std::filesystem::path GetAssetAbsolutePath(const std::filesystem::path& path); + void ReloadScriptEngine(); - static const std::filesystem::path& GetActiveProjectDirectory() + static Ref GetActive() { return s_ActiveProject; } + static void SetActive(Ref project); + static void SetActiveRuntime(Ref project, Ref assetPack); + + inline static Ref GetAssetManager() { return s_AssetManager; } + inline static Ref GetEditorAssetManager() { return s_AssetManager.As(); } + inline static Ref GetRuntimeAssetManager() { return s_AssetManager.As(); } + + static const std::string& GetProjectName() + { + SE_CORE_ASSERT(s_ActiveProject); + return s_ActiveProject->GetConfig().Name; + } + + static std::filesystem::path GetProjectDirectory() { SE_CORE_ASSERT(s_ActiveProject); - return s_ActiveProject->GetProjectDirectory(); + return s_ActiveProject->GetConfig().ProjectDirectory; + } + + std::filesystem::path GetAssetDirectory() const + { + return std::filesystem::path(GetConfig().ProjectDirectory) / GetConfig().AssetDirectory; } static std::filesystem::path GetActiveAssetDirectory() @@ -44,34 +89,69 @@ namespace StarEngine { return s_ActiveProject->GetAssetDirectory(); } - static std::filesystem::path GetActiveAssetRegistryPath() + static std::filesystem::path GetAssetRegistryPath() + { + SE_CORE_ASSERT(s_ActiveProject); + return std::filesystem::path(s_ActiveProject->GetConfig().ProjectDirectory) / s_ActiveProject->GetConfig().AssetRegistryPath; + } + + static std::filesystem::path GetMeshPath() { SE_CORE_ASSERT(s_ActiveProject); - return s_ActiveProject->GetAssetRegistryPath(); + return std::filesystem::path(s_ActiveProject->GetConfig().ProjectDirectory) / s_ActiveProject->GetConfig().MeshPath; } - // TODO: move to asset manager when we have one - static std::filesystem::path GetActiveAssetFileSystemPath(const std::filesystem::path& path) + static std::filesystem::path GetAnimationPath() { SE_CORE_ASSERT(s_ActiveProject); - return s_ActiveProject->GetAssetFileSystemPath(path); + return std::filesystem::path(s_ActiveProject->GetConfig().ProjectDirectory) / s_ActiveProject->GetConfig().AnimationPath; } + static std::filesystem::path GetAudioCommandsRegistryPath() + { + SE_CORE_ASSERT(s_ActiveProject); + return std::filesystem::path(s_ActiveProject->GetConfig().ProjectDirectory) / s_ActiveProject->GetConfig().AudioCommandsRegistryPath; + } - ProjectConfig& GetConfig() { return m_Config; } + static std::filesystem::path GetScriptModulePath() + { + SE_CORE_ASSERT(s_ActiveProject); + return std::filesystem::path(s_ActiveProject->GetConfig().ProjectDirectory) / s_ActiveProject->GetConfig().ScriptModulePath; + } - static Ref GetActive() { return s_ActiveProject; } - std::shared_ptr GetAssetManager() { return m_AssetManager; } - std::shared_ptr GetRuntimeAssetManager() { return std::static_pointer_cast(m_AssetManager); } - std::shared_ptr GetEditorAssetManager() { return std::static_pointer_cast(m_AssetManager); } + static std::filesystem::path GetScriptModuleFilePath() + { + SE_CORE_ASSERT(s_ActiveProject); + return GetScriptModulePath() / std::format("{0}.dll", GetProjectName()); + } + + std::filesystem::path GetScriptProjectPath() const + { + return GetAssetDirectory() / "Scripts" / (GetConfig().Name + ".csproj"); + } + + static std::filesystem::path GetActiveScriptProjectPath() + { + SE_CORE_ASSERT(s_ActiveProject); + return s_ActiveProject->GetScriptProjectPath(); + } + + static std::filesystem::path GetCacheDirectory() + { + SE_CORE_ASSERT(s_ActiveProject); + return std::filesystem::path(s_ActiveProject->GetConfig().ProjectDirectory) / "Cache"; + } + private: + void OnSerialized(); + void OnDeserialized(); - static Ref New(); - static Ref Load(const std::filesystem::path& path); - static bool SaveActive(const std::filesystem::path& path); private: ProjectConfig m_Config; - std::filesystem::path m_ProjectDirectory; - std::shared_ptr m_AssetManager; + Ref m_AudioCommands; + inline static Ref s_AssetManager; + + friend class ProjectSettingsWindow; + friend class ProjectSerializer; inline static Ref s_ActiveProject; }; diff --git a/StarEngine/src/StarEngine/Project/ProjectRuntimeFormat.h b/StarEngine/src/StarEngine/Project/ProjectRuntimeFormat.h new file mode 100644 index 00000000..6666d928 --- /dev/null +++ b/StarEngine/src/StarEngine/Project/ProjectRuntimeFormat.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "StarEngine/Core/UUID.h" +#include "StarEngine/Physics/PhysicsSettings.h" + +namespace StarEngine { + + struct ProjectInfo + { + struct FileHeader + { + const char HEADER[4] = { 'H','D','A','T' }; + uint32_t Version = 1; + }; + + struct Audio + { + double FileStreamingDurationThreshold; + }; + + FileHeader Header; + AssetHandle StartScene; + Audio AudioInfo; + // Physics + }; + +} diff --git a/StarEngine/src/StarEngine/Project/ProjectSerializer.cpp b/StarEngine/src/StarEngine/Project/ProjectSerializer.cpp index 65807bb9..7b17abc1 100644 --- a/StarEngine/src/StarEngine/Project/ProjectSerializer.cpp +++ b/StarEngine/src/StarEngine/Project/ProjectSerializer.cpp @@ -1,8 +1,18 @@ #include "sepch.h" #include "ProjectSerializer.h" +#include "ProjectRuntimeFormat.h" -#include -#include +#include "StarEngine/Physics/PhysicsSystem.h" +#include "StarEngine/Physics/PhysicsLayer.h" +#include "StarEngine/Audio/AudioEngine.h" + +#include "StarEngine/Utilities/YAMLSerializationHelpers.h" +#include "StarEngine/Utilities/SerializationMacros.h" +#include "StarEngine/Utilities/StringUtils.h" + +#include "yaml-cpp/yaml.h" + +#include namespace StarEngine { @@ -11,58 +21,468 @@ namespace StarEngine { { } - bool ProjectSerializer::Serialize(const std::filesystem::path& filepath) + void ProjectSerializer::Serialize(const std::filesystem::path& filepath) { - const auto& config = m_Project->GetConfig(); - YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "Project" << YAML::Value; { - out << YAML::BeginMap; // Root - out << YAML::Key << "Project" << YAML::Value; + out << YAML::BeginMap; + out << YAML::Key << "Name" << YAML::Value << m_Project->m_Config.Name; + out << YAML::Key << "AssetDirectory" << YAML::Value << m_Project->m_Config.AssetDirectory; + out << YAML::Key << "AssetRegistry" << YAML::Value << m_Project->m_Config.AssetRegistryPath; + out << YAML::Key << "AudioCommandsRegistryPath" << YAML::Value << m_Project->m_Config.AudioCommandsRegistryPath; + out << YAML::Key << "MeshPath" << YAML::Value << m_Project->m_Config.MeshPath; + out << YAML::Key << "MeshSourcePath" << YAML::Value << m_Project->m_Config.MeshSourcePath; + out << YAML::Key << "AnimationPath" << YAML::Value << m_Project->m_Config.AnimationPath; + + out << YAML::Key << "ScriptModulePath" << YAML::Value << m_Project->m_Config.ScriptModulePath; + out << YAML::Key << "DefaultNamespace" << YAML::Value << m_Project->m_Config.DefaultNamespace; + + out << YAML::Key << "StartScene" << YAML::Value << m_Project->m_Config.StartScene; + out << YAML::Key << "AutomaticallyReloadAssembly" << YAML::Value << m_Project->m_Config.AutomaticallyReloadAssembly; + out << YAML::Key << "AutoSave" << YAML::Value << m_Project->m_Config.EnableAutoSave; + out << YAML::Key << "AutoSaveInterval" << YAML::Value << m_Project->m_Config.AutoSaveIntervalSeconds; + + out << YAML::Key << "Audio" << YAML::Value; { - out << YAML::BeginMap;// Project - out << YAML::Key << "Name" << YAML::Value << config.Name; - out << YAML::Key << "StartScene" << YAML::Value << (uint64_t)config.StartScene; - out << YAML::Key << "AssetDirectory" << YAML::Value << config.AssetDirectory.string(); - out << YAML::Key << "AssetRegistryPath" << YAML::Value << config.AssetRegistryPath.string(); - out << YAML::Key << "ScriptModulePath" << YAML::Value << config.ScriptModulePath.string(); - out << YAML::EndMap; // Project + out << YAML::BeginMap; + auto userConfig = AudioEngine::GetEngine(); + SE_SERIALIZE_PROPERTY(FileStreamingDurationThreshold, userConfig.FileStreamingDurationThreshold, out); + out << YAML::EndMap; } - out << YAML::EndMap; // Root + + out << YAML::Key << "Physics" << YAML::Value; + { + out << YAML::BeginMap; + + const auto& physicsSettings = PhysicsSystem::GetSettings(); + + out << YAML::Key << "FixedTimestep" << YAML::Value << physicsSettings.FixedTimestep; + out << YAML::Key << "Gravity" << YAML::Value << physicsSettings.Gravity; + out << YAML::Key << "SolverPositionIterations" << YAML::Value << physicsSettings.PositionSolverIterations; + out << YAML::Key << "SolverVelocityIterations" << YAML::Value << physicsSettings.VelocitySolverIterations; + out << YAML::Key << "MaxBodies" << YAML::Value << physicsSettings.MaxBodies; + + out << YAML::Key << "CaptureOnPlay" << YAML::Value << physicsSettings.CaptureOnPlay; + out << YAML::Key << "CaptureMethod" << YAML::Value << (int)physicsSettings.CaptureMethod; + + // > 1 because of the Default layer + if (PhysicsLayerManager::GetLayerCount() > 1) + { + out << YAML::Key << "Layers"; + out << YAML::Value << YAML::BeginSeq; + for (const auto& layer : PhysicsLayerManager::GetLayers()) + { + // Never serialize the Default layer + if (layer.LayerID == 0) + continue; + + out << YAML::BeginMap; + out << YAML::Key << "Name" << YAML::Value << layer.Name; + out << YAML::Key << "CollidesWithSelf" << YAML::Value << layer.CollidesWithSelf; + out << YAML::Key << "CollidesWith" << YAML::Value; + out << YAML::BeginSeq; + for (const auto& collidingLayer : PhysicsLayerManager::GetLayerCollisions(layer.LayerID)) + { + out << YAML::BeginMap; + out << YAML::Key << "Name" << YAML::Value << collidingLayer.Name; + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + + out << YAML::EndMap; + } + + out << YAML::Key << "Log" << YAML::Value; + { + out << YAML::BeginMap; + auto& tags = Log::EnabledTags(); + for (auto& [name, details] : tags) + { + if (!name.empty()) // Don't serialize untagged log + { + out << YAML::Key << name << YAML::Value; + out << YAML::BeginMap; + { + out << YAML::Key << "Enabled" << YAML::Value << details.Enabled; + out << YAML::Key << "LevelFilter" << YAML::Value << Log::LevelToString(details.LevelFilter); + out << YAML::EndMap; + } + + } + } + out << YAML::EndMap; + } + + out << YAML::EndMap; } + out << YAML::EndMap; std::ofstream fout(filepath); fout << out.c_str(); + m_Project->OnSerialized(); + } + + bool ProjectSerializer::SerializeRuntime(const std::filesystem::path& filepath) + { + ProjectInfo projectInfo; + + // Start Scene + { + auto& config = m_Project->m_Config; + AssetHandle startSceneID = 0; + if (!config.StartScene.empty()) + { + startSceneID = Project::GetEditorAssetManager()->GetAssetHandleFromFilePath(config.StartScene); + } + + if (startSceneID == 0) + { + SE_CORE_ERROR("Error building runtime project - no start scene could be found! (StartScene: {})", config.StartScene); + return false; + } + projectInfo.StartScene = startSceneID; + } + + // Audio + { + auto userConfig = MiniAudioEngine::Get().GetUserConfiguration(); + projectInfo.AudioInfo.FileStreamingDurationThreshold = userConfig.FileStreamingDurationThreshold; + } + + + FileStreamWriter serializer(filepath); + serializer.WriteRaw(projectInfo); + + // Physics + const auto& physicsSettings = PhysicsSystem::GetSettings(); + + serializer.WriteRaw(physicsSettings.FixedTimestep); + serializer.WriteRaw(physicsSettings.Gravity); + serializer.WriteRaw(physicsSettings.PositionSolverIterations); + serializer.WriteRaw(physicsSettings.VelocitySolverIterations); + serializer.WriteRaw(physicsSettings.MaxBodies); + + uint32_t physicsLayerCount = PhysicsLayerManager::GetLayerCount(); + serializer.WriteRaw(physicsLayerCount - 1); // NOTE(Yan): don't include default layer + if (physicsLayerCount > 1) + { + const auto& layers = PhysicsLayerManager::GetLayers(); + + // Write only names first + for (const auto& layer : layers) + { + if (layer.LayerID == 0) + continue; + + serializer.WriteString(layer.Name); + } + + // Write remaining data + for (const auto& layer : layers) + { + if (layer.LayerID == 0) + continue; + + serializer.WriteRaw(layer.CollidesWithSelf); + + const auto& layerCollisions = PhysicsLayerManager::GetLayerCollisions(layer.LayerID); + std::vector collisionLayerNames(layerCollisions.size()); + for (size_t i = 0; i < layerCollisions.size(); i++) + { + collisionLayerNames[i] = layerCollisions[i].Name; + } + serializer.WriteArray(collisionLayerNames); + } + + } + + // Logging + // NOTE: these can be overidden in runtime with an external text file, the project file + // defines default levels for each tag + { + auto& tags = Log::EnabledTags(); + + uint32_t count = 0; + for (auto& [name, details] : tags) + { + if (!name.empty()) // Don't serialize untagged log + count++; + } + serializer.WriteRaw(count); + for (auto& [name, details] : tags) + { + if (!name.empty()) // Don't serialize untagged log + { + serializer.WriteString(name); + serializer.WriteRaw(details.Enabled); + serializer.WriteRaw((uint8_t)details.LevelFilter); + } + } + } + + return true; } bool ProjectSerializer::Deserialize(const std::filesystem::path& filepath) { - auto& config = m_Project->GetConfig(); + PhysicsLayerManager::ClearLayers(); + PhysicsLayerManager::AddLayer("Default"); + + std::ifstream stream(filepath); + SE_CORE_ASSERT(stream); + std::stringstream strStream; + strStream << stream.rdbuf(); + + YAML::Node data = YAML::Load(strStream.str()); + if (!data["Project"]) + return false; + + YAML::Node rootNode = data["Project"]; + if (!rootNode["Name"]) + return false; - YAML::Node data; - try + auto& config = m_Project->m_Config; + config.Name = rootNode["Name"].as(); + + config.AssetDirectory = rootNode["AssetDirectory"].as(); + config.AssetRegistryPath = rootNode["AssetRegistry"].as(); + + std::filesystem::path projectPath = filepath; + config.ProjectFileName = projectPath.filename().string(); + config.ProjectDirectory = projectPath.parent_path().string(); + + if (rootNode["AudioCommandsRegistryPath"]) + config.AudioCommandsRegistryPath = rootNode["AudioCommandsRegistryPath"].as(); + + config.StartScene = rootNode["StartScene"].as(""); + + config.MeshPath = rootNode["MeshPath"].as(); + config.MeshSourcePath = rootNode["MeshSourcePath"].as(); + + config.AnimationPath = rootNode["AnimationPath"].as("Assets/Animation"); + + config.ScriptModulePath = rootNode["ScriptModulePath"].as(config.ScriptModulePath); + + config.DefaultNamespace = rootNode["DefaultNamespace"].as(config.Name); + config.AutomaticallyReloadAssembly = rootNode["AutomaticallyReloadAssembly"].as(true); + + config.EnableAutoSave = rootNode["AutoSave"].as(false); + config.AutoSaveIntervalSeconds = rootNode["AutoSaveInterval"].as(300); + + // Audio + auto audioNode = rootNode["Audio"]; + if (audioNode) { - data = YAML::LoadFile(filepath.string()); + auto userConfig = MiniAudioEngine::Get().GetUserConfiguration(); + SE_DESERIALIZE_PROPERTY(FileStreamingDurationThreshold, userConfig.FileStreamingDurationThreshold, audioNode, userConfig.FileStreamingDurationThreshold); + MiniAudioEngine::Get().SetUserConfiguration(userConfig); } - catch (YAML::ParserException e) + + // Physics + auto physicsNode = rootNode["Physics"]; + if (physicsNode) { - SE_CORE_ERROR("Failed to load project file '{0}'\n {1}", filepath.string(), e.what()); + auto& physicsSettings = PhysicsSystem::GetSettings(); + + physicsSettings.FixedTimestep = physicsNode["FixedTimestep"].as(1.0f / 60.0f); + physicsSettings.Gravity = physicsNode["Gravity"].as(glm::vec3(0.0f, -9.81f, 0.0f)); + physicsSettings.PositionSolverIterations = physicsNode["SolverPositionIterations"].as(8); + physicsSettings.VelocitySolverIterations = physicsNode["SolverVelocityIterations"].as(2); + + physicsSettings.MaxBodies = physicsNode["MaxBodies"].as(1000); + + physicsSettings.CaptureOnPlay = physicsNode["CaptureOnPlay"].as(true); + physicsSettings.CaptureMethod = (PhysicsDebugType)physicsNode["CaptureMethod"].as((int)PhysicsDebugType::LiveDebug); + + auto physicsLayers = physicsNode["Layers"]; + + if (!physicsLayers) + physicsLayers = physicsNode["PhysicsLayers"]; // Temporary fix since I accidentially serialized physics layers under the wrong name until now... Will remove this in a month or so once most projects have been changed to the proper name + + if (physicsLayers) + { + for (auto layer : physicsLayers) + PhysicsLayerManager::AddLayer(layer["Name"].as(), false); + + for (auto layer : physicsLayers) + { + PhysicsLayer& layerInfo = PhysicsLayerManager::GetLayer(layer["Name"].as()); + layerInfo.CollidesWithSelf = layer["CollidesWithSelf"].as(true); + + auto collidesWith = layer["CollidesWith"]; + if (collidesWith) + { + for (auto collisionLayer : collidesWith) + { + const auto& otherLayer = PhysicsLayerManager::GetLayer(collisionLayer["Name"].as()); + PhysicsLayerManager::SetLayerCollision(layerInfo.LayerID, otherLayer.LayerID, true); + } + } + } + } + } + + // Log + auto logNode = rootNode["Log"]; + if (logNode) + { + auto& tags = Log::EnabledTags(); + for (auto node : logNode) + { + std::string name = node.first.as(); + auto& details = tags[name]; + details.Enabled = node.second["Enabled"].as(); + details.LevelFilter = Log::LevelFromString(node.second["LevelFilter"].as()); + } + } + + m_Project->OnDeserialized(); + return true; + } + + bool ProjectSerializer::DeserializeRuntime(const std::filesystem::path& filepath) + { + PhysicsLayerManager::ClearLayers(); + PhysicsLayerManager::AddLayer("Default"); + + SE_CORE_TRACE_TAG("Project", "Deserializing Project from {}", filepath.string()); + FileStreamReader stream(filepath); + if (!stream.IsStreamGood()) + return false; + + ProjectInfo projectInfo; + stream.ReadRaw(projectInfo); + bool validHeader = memcmp(projectInfo.Header.HEADER, "HDAT", 4) == 0; + SE_CORE_VERIFY(validHeader); + if (!validHeader) + { + char invalidHeader[4]; + memcpy(invalidHeader, projectInfo.Header.HEADER, 4); + SE_CORE_ERROR_TAG("Project", "Project file has invalid header ({})", invalidHeader); return false; } - auto projectNode = data["Project"]; - if (!projectNode) + ProjectInfo current; + if (projectInfo.Header.Version != current.Header.Version) + { + SE_CORE_ERROR_TAG("Project", "Project version {} is not compatible with current version {}", projectInfo.Header.Version, current.Header.Version); return false; + } + + // Set project config + auto& config = m_Project->m_Config; + config.ProjectDirectory = filepath.parent_path().string(); + config.StartSceneHandle = projectInfo.StartScene; + + // Audio + { + auto userConfig = MiniAudioEngine::Get().GetUserConfiguration(); + userConfig.FileStreamingDurationThreshold = projectInfo.AudioInfo.FileStreamingDurationThreshold; + MiniAudioEngine::Get().SetUserConfiguration(userConfig); + } + + // Physics + { + auto& physicsSettings = PhysicsSystem::GetSettings(); + + stream.ReadRaw(physicsSettings.FixedTimestep); + stream.ReadRaw(physicsSettings.Gravity); + stream.ReadRaw(physicsSettings.PositionSolverIterations); + stream.ReadRaw(physicsSettings.VelocitySolverIterations); + stream.ReadRaw(physicsSettings.MaxBodies); + + uint32_t physicsLayerCount = PhysicsLayerManager::GetLayerCount(); + stream.ReadRaw(physicsLayerCount); + if (physicsLayerCount > 1) + { + std::vector layerNameStrings(physicsLayerCount); + for (uint32_t i = 0; i < physicsLayerCount; i++) + { + stream.ReadString(layerNameStrings[i]); + PhysicsLayerManager::AddLayer(layerNameStrings[i], false); + } + + for (uint32_t i = 0; i < physicsLayerCount; i++) + { + PhysicsLayer& layerInfo = PhysicsLayerManager::GetLayer(layerNameStrings[i]); + stream.ReadRaw(layerInfo.CollidesWithSelf); + + std::vector collisionLayerNames; + stream.ReadArray(collisionLayerNames); + for (auto& collisionLayer : collisionLayerNames) + { + const auto& otherLayer = PhysicsLayerManager::GetLayer(collisionLayer); + PhysicsLayerManager::SetLayerCollision(layerInfo.LayerID, otherLayer.LayerID, true); + } + + } + } + } + + // Logging + // NOTE: these can be overidden in runtime with an external text file, the project file + // defines default levels for each tag + { + auto& tags = Log::EnabledTags(); + + uint32_t count; + stream.ReadRaw(count); + + for (uint32_t i = 0; i < count; i++) + { + std::string name; + stream.ReadString(name); + + auto& details = tags[name]; + stream.ReadRaw(details.Enabled); + stream.ReadRaw(details.LevelFilter); + } + } + + // + // OVERRIDES + // + // Hazel allows certain settings to be overridden via a YAML file called Project.yaml, + // such as logging levels. + const std::filesystem::path overridesFile = filepath.parent_path() / "Project.yaml"; + if (FileSystem::Exists(overridesFile)) + { + std::ifstream stream(overridesFile); + SE_CORE_VERIFY(stream); + std::stringstream strStream; + strStream << stream.rdbuf(); + + YAML::Node data = YAML::Load(strStream.str()); + if (!data["Project"]) + return false; + + YAML::Node rootNode = data["Project"]; + + // Log + auto logNode = rootNode["Log"]; + if (logNode) + { + auto& tags = Log::EnabledTags(); + for (auto node : logNode) + { + std::string name = node.first.as(); + auto& details = tags[name]; + details.Enabled = node.second["Enabled"].as(); + details.LevelFilter = Log::LevelFromString(node.second["LevelFilter"].as()); + } + } + + } - config.Name = projectNode["Name"].as(); - config.StartScene = projectNode["StartScene"].as(); - config.AssetDirectory = projectNode["AssetDirectory"].as(); - if (projectNode["AssetRegistryPath"]) - config.AssetRegistryPath = projectNode["AssetRegistryPath"].as(); - config.ScriptModulePath = projectNode["ScriptModulePath"].as(); return true; } diff --git a/StarEngine/src/StarEngine/Project/ProjectSerializer.h b/StarEngine/src/StarEngine/Project/ProjectSerializer.h index 88d89c95..db4163af 100644 --- a/StarEngine/src/StarEngine/Project/ProjectSerializer.h +++ b/StarEngine/src/StarEngine/Project/ProjectSerializer.h @@ -2,6 +2,8 @@ #include "Project.h" +#include + namespace StarEngine { class ProjectSerializer @@ -9,8 +11,11 @@ namespace StarEngine { public: ProjectSerializer(Ref project); - bool Serialize(const std::filesystem::path& filepath); + void Serialize(const std::filesystem::path& filepath); + bool SerializeRuntime(const std::filesystem::path& filepath); bool Deserialize(const std::filesystem::path& filepath); + bool DeserializeRuntime(const std::filesystem::path& filepath); + private: Ref m_Project; }; diff --git a/StarEngine/src/StarEngine/Project/TieringSettings.cpp b/StarEngine/src/StarEngine/Project/TieringSettings.cpp new file mode 100644 index 00000000..b982be48 --- /dev/null +++ b/StarEngine/src/StarEngine/Project/TieringSettings.cpp @@ -0,0 +1,6 @@ +#include "sepch.h" +#include "TieringSettings.h" + +namespace Hazel { + +} diff --git a/StarEngine/src/StarEngine/Project/TieringSettings.h b/StarEngine/src/StarEngine/Project/TieringSettings.h new file mode 100644 index 00000000..b51163ba --- /dev/null +++ b/StarEngine/src/StarEngine/Project/TieringSettings.h @@ -0,0 +1,155 @@ +#pragma once + +#include + +namespace StarEngine { + + namespace Tiering { + + namespace Renderer { + + enum class ShadowQualitySetting + { + None = 0, Low = 1, High = 2 + }; + + enum class ShadowResolutionSetting + { + None = 0, Low = 1, Medium = 2, High = 3 + }; + + enum class AmbientOcclusionTypeSetting + { + None = 0, GTAO = 1 + }; + + enum class AmbientOcclusionQualitySetting + { + None = 0, High = 1, Ultra = 2 + }; + + enum class SSRQualitySetting + { + Off = 0, Medium = 1, High = 2 + }; + + struct RendererTieringSettings + { + float RendererScale = 1.0f; + bool Windowed = false; + bool VSync = true; + + // Shadows + bool EnableShadows = true; + Renderer::ShadowQualitySetting ShadowQuality = Renderer::ShadowQualitySetting::High; + Renderer::ShadowResolutionSetting ShadowResolution = Renderer::ShadowResolutionSetting::High; + + // Ambient Occlusion + bool EnableAO = true; + Renderer::AmbientOcclusionTypeSetting AOType = Renderer::AmbientOcclusionTypeSetting::GTAO; + Renderer::AmbientOcclusionQualitySetting AOQuality = Renderer::AmbientOcclusionQualitySetting::Ultra; + Renderer::SSRQualitySetting SSRQuality = Renderer::SSRQualitySetting::Off; + + bool EnableBloom = true; + }; + + namespace Utils { + + inline const char* ShadowQualitySettingToString(ShadowQualitySetting shadowQualitySetting) + { + switch (shadowQualitySetting) + { + case ShadowQualitySetting::None: return "None"; + case ShadowQualitySetting::Low: return "Low"; + case ShadowQualitySetting::High: return "High"; + } + + return nullptr; + } + + inline ShadowQualitySetting ShadowQualitySettingFromString(std::string_view shadowQualitySetting) + { + if (shadowQualitySetting == "None") return ShadowQualitySetting::None; + if (shadowQualitySetting == "Low") return ShadowQualitySetting::Low; + if (shadowQualitySetting == "High") return ShadowQualitySetting::High; + + return ShadowQualitySetting::None; + } + + inline const char* ShadowResolutionSettingToString(ShadowResolutionSetting shadowResolutionSetting) + { + switch (shadowResolutionSetting) + { + case ShadowResolutionSetting::None: return "None"; + case ShadowResolutionSetting::Low: return "Low"; + case ShadowResolutionSetting::Medium: return "Medium"; + case ShadowResolutionSetting::High: return "High"; + } + + return nullptr; + } + + inline ShadowResolutionSetting ShadowResolutionSettingFromString(std::string_view shadowResolutionSetting) + { + if (shadowResolutionSetting == "None") return ShadowResolutionSetting::None; + if (shadowResolutionSetting == "Low") return ShadowResolutionSetting::Low; + if (shadowResolutionSetting == "Medium") return ShadowResolutionSetting::Medium; + if (shadowResolutionSetting == "High") return ShadowResolutionSetting::High; + + return ShadowResolutionSetting::None; + } + + inline const char* AmbientOcclusionQualitySettingToString(AmbientOcclusionQualitySetting ambientOcclusionQualitySetting) + { + switch (ambientOcclusionQualitySetting) + { + case AmbientOcclusionQualitySetting::None: return "None"; + case AmbientOcclusionQualitySetting::High: return "High"; + case AmbientOcclusionQualitySetting::Ultra: return "Ultra"; + } + + return nullptr; + } + + inline AmbientOcclusionQualitySetting AmbientOcclusionQualitySettingFromString(std::string_view ambientOcclusionQualitySetting) + { + if (ambientOcclusionQualitySetting == "None") return AmbientOcclusionQualitySetting::None; + if (ambientOcclusionQualitySetting == "Low") return AmbientOcclusionQualitySetting::High; // NOTE(Yan): low has been renamed to high, currently there is no low + if (ambientOcclusionQualitySetting == "High") return AmbientOcclusionQualitySetting::High; + if (ambientOcclusionQualitySetting == "Ultra") return AmbientOcclusionQualitySetting::Ultra; + + return AmbientOcclusionQualitySetting::None; + } + + inline const char* SSRQualitySettingToString(SSRQualitySetting ssrQualitySetting) + { + switch (ssrQualitySetting) + { + case SSRQualitySetting::Off: return "Off"; + case SSRQualitySetting::Medium: return "Medium"; + case SSRQualitySetting::High: return "High"; + } + + return nullptr; + } + + inline SSRQualitySetting SSRQualitySettingFromString(std::string_view ssrQualitySetting) + { + if (ssrQualitySetting == "Off") return SSRQualitySetting::Off; + if (ssrQualitySetting == "Medium") return SSRQualitySetting::Medium; + if (ssrQualitySetting == "High") return SSRQualitySetting::High; + + return SSRQualitySetting::Off; + } + + } + } + + struct TieringSettings + { + Renderer::RendererTieringSettings RendererTS; + }; + + } + +} diff --git a/StarEngine/src/StarEngine/Project/UserPreferences.cpp b/StarEngine/src/StarEngine/Project/UserPreferences.cpp new file mode 100644 index 00000000..2d60f1b7 --- /dev/null +++ b/StarEngine/src/StarEngine/Project/UserPreferences.cpp @@ -0,0 +1,85 @@ +#include "sepch.h" +#include "UserPreferences.h" + +#include "yaml-cpp/yaml.h" + +#include +#include + +namespace StarEngine { + + UserPreferencesSerializer::UserPreferencesSerializer(const Ref& preferences) + : m_Preferences(preferences) + { + } + + UserPreferencesSerializer::~UserPreferencesSerializer() + { + } + + void UserPreferencesSerializer::Serialize(const std::filesystem::path& filepath) + { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "UserPrefs" << YAML::Value; + { + out << YAML::BeginMap; + out << YAML::Key << "ShowWelcomeScreen" << YAML::Value << m_Preferences->ShowWelcomeScreen; + + if (!m_Preferences->StartupProject.empty()) + out << YAML::Key << "StartupProject" << YAML::Value << m_Preferences->StartupProject; + + { + out << YAML::Key << "RecentProjects"; + out << YAML::Value << YAML::BeginSeq; + for (const auto&[lastOpened, projectConfig] : m_Preferences->RecentProjects) + { + out << YAML::BeginMap; + out << YAML::Key << "Name" << YAML::Value << projectConfig.Name; + out << YAML::Key << "ProjectPath" << YAML::Value << projectConfig.FilePath; + out << YAML::Key << "LastOpened" << YAML::Value << projectConfig.LastOpened; + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + + out << YAML::EndMap; + } + out << YAML::EndMap; + + std::ofstream fout(filepath); + fout << out.c_str(); + + m_Preferences->FilePath = filepath.string(); + } + + void UserPreferencesSerializer::Deserialize(const std::filesystem::path& filepath) + { + std::ifstream stream(filepath); + SE_CORE_ASSERT(stream); + std::stringstream strStream; + strStream << stream.rdbuf(); + + YAML::Node data = YAML::Load(strStream.str()); + if (!data["UserPrefs"]) + return; + + YAML::Node rootNode = data["UserPrefs"]; + m_Preferences->ShowWelcomeScreen = rootNode["ShowWelcomeScreen"].as(); + m_Preferences->StartupProject = rootNode["StartupProject"] ? rootNode["StartupProject"].as() : ""; + + for (auto recentProject : rootNode["RecentProjects"]) + { + RecentProject entry; + entry.Name = recentProject["Name"].as(); + entry.FilePath = recentProject["ProjectPath"].as(); + entry.LastOpened = recentProject["LastOpened"] ? recentProject["LastOpened"].as() : time(NULL); + m_Preferences->RecentProjects[entry.LastOpened] = entry; + } + + stream.close(); + + m_Preferences->FilePath = filepath.string(); + } + +} diff --git a/StarEngine/src/StarEngine/Project/UserPreferences.h b/StarEngine/src/StarEngine/Project/UserPreferences.h new file mode 100644 index 00000000..ce3ed12f --- /dev/null +++ b/StarEngine/src/StarEngine/Project/UserPreferences.h @@ -0,0 +1,40 @@ +#pragma once + +#include "StarEngine/Core/Log.h" +#include "StarEngine/Project/Project.h" + +#include + +namespace StarEngine { + + struct RecentProject + { + std::string Name; + std::string FilePath; + time_t LastOpened; + }; + + struct UserPreferences : public RefCounted + { + bool ShowWelcomeScreen = true; + std::string StartupProject; + std::map> RecentProjects; + + // Not Serialized + std::string FilePath; + }; + + class UserPreferencesSerializer + { + public: + UserPreferencesSerializer(const Ref& preferences); + ~UserPreferencesSerializer(); + + void Serialize(const std::filesystem::path& filepath); + void Deserialize(const std::filesystem::path& filepath); + + private: + Ref m_Preferences; + }; + +} diff --git a/StarEngine/src/StarEngine/Reflection/MetaHelpers.h b/StarEngine/src/StarEngine/Reflection/MetaHelpers.h new file mode 100644 index 00000000..1c0652f9 --- /dev/null +++ b/StarEngine/src/StarEngine/Reflection/MetaHelpers.h @@ -0,0 +1,475 @@ +#pragma once + +#include + +//============================================================================== +/* + A bunch of useful meta-programming utilities, directly taken from + header in case they are removed in the future. +*/ +namespace meta { + //============================================================================== + template + struct All_same : std::true_type {}; + // variadic is_same + template + struct All_same : std::bool_constant...>> {}; + + // variadic is_convertible + template + struct Convertible_from_all : std::bool_constant...>> + { + }; + + //============================================================================== + // A sequence of types + template + struct Meta_list {}; + struct Meta_nil {}; + + template + struct Meta_list_i {}; + + template + struct Meta_front_; + // Extract the first type in a sequence (head of list) + template + using Meta_front = + typename Meta_front_::type; + + template