Преглед изворни кода

Atom Tools: MCP added project

Signed-off-by: Guthrie Adams <[email protected]>
Guthrie Adams пре 3 година
родитељ
комит
c681ce014a
100 измењених фајлова са 5445 додато и 1 уклоњено
  1. 0 1
      Gems/Atom/Tools/CMakeLists.txt
  2. 3 0
      Gems/Atom/Tools/MaterialCanvas/Assets/Editor/Icons/material.png
  3. 3 0
      Gems/Atom/Tools/MaterialCanvas/Assets/Editor/Icons/matgroup.png
  4. 13 0
      Gems/Atom/Tools/MaterialCanvas/CMakeLists.txt
  5. 84 0
      Gems/Atom/Tools/MaterialCanvas/Code/CMakeLists.txt
  6. 180 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocument.cpp
  7. 70 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocument.h
  8. 22 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocumentRequestBus.h
  9. 47 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocumentSettings.cpp
  10. 30 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocumentSettings.h
  11. 127 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/MaterialCanvasApplication.cpp
  12. 54 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/MaterialCanvasApplication.h
  13. 9 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Android/PAL_android.cmake
  14. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Android/platform_android_files.cmake
  15. 8 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/MaterialCanvas_Traits_Linux.h
  16. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/MaterialCanvas_Traits_Platform.h
  17. 9 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/PAL_linux.cmake
  18. 12 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/platform_linux_files.cmake
  19. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/tool_dependencies_linux.cmake
  20. 8 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/MaterialCanvas_Traits_Mac.h
  21. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/MaterialCanvas_Traits_Platform.h
  22. 9 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/PAL_mac.cmake
  23. 12 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/platform_mac_files.cmake
  24. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/tool_dependencies_mac.cmake
  25. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas.ico
  26. 1 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas.rc
  27. 14 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas_Traits_Platform.h
  28. 8 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas_Traits_Windows.h
  29. 9 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/PAL_windows.cmake
  30. 13 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/platform_windows_files.cmake
  31. 11 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/tool_dependencies_windows.cmake
  32. 9 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/iOS/PAL_ios.cmake
  33. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/iOS/platform_ios_files.cmake
  34. 166 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/Behavior.cpp
  35. 64 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/Behavior.h
  36. 36 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/DollyCameraBehavior.cpp
  37. 30 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/DollyCameraBehavior.h
  38. 13 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/IdleBehavior.cpp
  39. 22 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/IdleBehavior.h
  40. 352 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MaterialCanvasViewportInputController.cpp
  41. 109 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MaterialCanvasViewportInputController.h
  42. 61 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MaterialCanvasViewportInputControllerBus.h
  43. 56 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MoveCameraBehavior.cpp
  44. 32 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MoveCameraBehavior.h
  45. 59 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/OrbitCameraBehavior.cpp
  46. 36 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/OrbitCameraBehavior.h
  47. 56 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/PanCameraBehavior.cpp
  48. 33 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/PanCameraBehavior.h
  49. 51 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateEnvironmentBehavior.cpp
  50. 45 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateEnvironmentBehavior.h
  51. 59 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateModelBehavior.cpp
  52. 36 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateModelBehavior.h
  53. 482 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportComponent.cpp
  54. 116 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportComponent.h
  55. 30 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportModule.cpp
  56. 28 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportModule.h
  57. 71 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportNotificationBus.h
  58. 138 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportRequestBus.h
  59. 68 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportSettings.cpp
  60. 37 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportSettings.h
  61. 475 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportWidget.cpp
  62. 119 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportWidget.h
  63. 50 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportWidget.ui
  64. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/add.png
  65. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/addnew.png
  66. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/down.png
  67. 9 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/grid.svg
  68. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/material.svg
  69. 15 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/materialcanvas.svg
  70. 7 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/materialtype.svg
  71. 6 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/mesh.svg
  72. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/refresh.png
  73. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/remove.png
  74. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save.png
  75. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save_active.png
  76. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save_disabled.png
  77. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save_normal.png
  78. 9 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/shadow.svg
  79. 6 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/skybox.svg
  80. 10 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/texture.svg
  81. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/texture_edit.png
  82. 14 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/toneMapping.svg
  83. 3 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/up.png
  84. 235 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Inspector/MaterialCanvasInspector.cpp
  85. 74 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Inspector/MaterialCanvasInspector.h
  86. 25 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvas.qrc
  87. 14 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvas.qss
  88. 353 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasBrowserInteractions.cpp
  89. 58 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasBrowserInteractions.h
  90. 190 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindow.cpp
  91. 53 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindow.h
  92. 46 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindowSettings.cpp
  93. 32 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindowSettings.h
  94. 105 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/LightingPresetComboBox.cpp
  95. 38 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/LightingPresetComboBox.h
  96. 136 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/MaterialCanvasToolBar.cpp
  97. 42 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/MaterialCanvasToolBar.h
  98. 105 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/ModelPresetComboBox.cpp
  99. 38 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/ModelPresetComboBox.h
  100. 24 0
      Gems/Atom/Tools/MaterialCanvas/Code/Source/main.cpp

+ 0 - 1
Gems/Atom/Tools/CMakeLists.txt

@@ -5,4 +5,3 @@
 # SPDX-License-Identifier: Apache-2.0 OR MIT
 #
 #
-

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Assets/Editor/Icons/material.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd5106eebb6cf264fdac5f3568977fc8f944df4a4d4de6b0e9f35b3a001a3001
+size 206

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Assets/Editor/Icons/matgroup.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9d4df90159dd647379d2e279af9ef443dd071f1aa04da2b3cb4bc777cdd882f0
+size 247

+ 13 - 0
Gems/Atom/Tools/MaterialCanvas/CMakeLists.txt

@@ -0,0 +1,13 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(gem_path ${CMAKE_CURRENT_LIST_DIR})
+set(gem_json ${gem_path}/gem.json)
+o3de_restricted_path(${gem_json} gem_restricted_path gem_parent_relative_path)
+
+add_subdirectory(Code)

+ 84 - 0
Gems/Atom/Tools/MaterialCanvas/Code/CMakeLists.txt

@@ -0,0 +1,84 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+if(NOT PAL_TRAIT_BUILD_HOST_TOOLS)
+    return()
+endif()
+
+o3de_pal_dir(pal_source_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME} ${gem_restricted_path} ${gem_path} ${gem_parent_relative_path})
+
+include(${pal_source_dir}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) # PAL_TRAIT_ATOM_MATERIAL_CANVAS_APPLICATION_SUPPORTED
+
+if(NOT PAL_TRAIT_ATOM_MATERIAL_CANVAS_APPLICATION_SUPPORTED)
+    return()
+endif()
+
+ly_add_target(
+    NAME MaterialCanvas EXECUTABLE
+    NAMESPACE Gem
+    AUTOMOC
+    AUTOUIC
+    AUTORCC
+    FILES_CMAKE
+        materialcanvas_files.cmake
+        ${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake
+    PLATFORM_INCLUDE_FILES
+        ${pal_source_dir}/tool_dependencies_${PAL_PLATFORM_NAME_LOWERCASE}.cmake
+    INCLUDE_DIRECTORIES
+        PRIVATE
+            .
+            Source
+            ${pal_source_dir}
+        PUBLIC
+            Include
+    BUILD_DEPENDENCIES
+        PRIVATE
+            Gem::AtomToolsFramework.Static
+            Gem::AtomToolsFramework.Editor
+            Gem::Atom_RHI.Public
+            Gem::Atom_RHI.Reflect
+            Gem::Atom_RPI.Edit
+            Gem::Atom_RPI.Public
+            Gem::Atom_Feature_Common.Public
+            Gem::Atom_Component_DebugCamera.Static
+            Gem::AtomLyIntegration_CommonFeatures.Static
+    RUNTIME_DEPENDENCIES
+        Gem::AtomToolsFramework.Editor
+        Gem::EditorPythonBindings.Editor
+)
+
+ly_set_gem_variant_to_load(TARGETS MaterialCanvas VARIANTS Tools)
+
+# Add a 'builders' alias to allow the MaterialCanvas root gem path to be added to the generated
+# cmake_dependencies.<project>.assetprocessor.setreg to allow the asset scan folder for it to be added
+ly_create_alias(NAME MaterialCanvas.Builders NAMESPACE Gem)
+
+# Add build dependency to Editor for the MaterialCanvas application since
+# Editor opens up the MaterialCanvas
+ly_add_dependencies(Editor Gem::MaterialCanvas)
+
+# Inject the project path into the MaterialCanvas VS debugger command arguments if the build system being invoked
+# in a project centric view
+if(NOT PROJECT_NAME STREQUAL "O3DE")
+    set_property(TARGET MaterialCanvas APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${CMAKE_SOURCE_DIR}\"")
+endif()
+
+# Adds the MaterialCanvas target as a C preprocessor define so that it can be used as a Settings Registry
+# specialization in order to look up the generated .setreg which contains the dependencies
+# specified for the target.
+if(TARGET MaterialCanvas)
+    set_source_files_properties(
+        Source/MaterialCanvasApplication.cpp
+        PROPERTIES
+            COMPILE_DEFINITIONS
+                LY_CMAKE_TARGET="MaterialCanvas"
+    )
+else()
+    message(FATAL_ERROR "Cannot set LY_CMAKE_TARGET define to MaterialCanvas as the target doesn't exist anymore."
+        " Perhaps it has been renamed")
+endif()

+ 180 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocument.cpp

@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/RPI.Edit/Common/AssetUtils.h>
+#include <Atom/RPI.Edit/Common/JsonUtils.h>
+#include <AtomToolsFramework/Document/AtomToolsDocumentNotificationBus.h>
+#include <AtomToolsFramework/Util/MaterialPropertyUtil.h>
+#include <Document/MaterialCanvasDocument.h>
+
+namespace MaterialCanvas
+{
+    MaterialCanvasDocument::MaterialCanvasDocument()
+        : AtomToolsFramework::AtomToolsDocument()
+    {
+        MaterialCanvasDocumentRequestBus::Handler::BusConnect(m_id);
+    }
+
+    MaterialCanvasDocument::~MaterialCanvasDocument()
+    {
+        MaterialCanvasDocumentRequestBus::Handler::BusDisconnect();
+    }
+
+    const AZStd::any& MaterialCanvasDocument::GetPropertyValue(const AZ::Name& propertyId) const
+    {
+        if (!IsOpen())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document is not open.");
+            return m_invalidValue;
+        }
+
+        const auto it = m_properties.find(propertyId);
+        if (it == m_properties.end())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document property could not be found: '%s'.", propertyId.GetCStr());
+            return m_invalidValue;
+        }
+
+        const AtomToolsFramework::DynamicProperty& property = it->second;
+        return property.GetValue();
+    }
+
+    const AtomToolsFramework::DynamicProperty& MaterialCanvasDocument::GetProperty(const AZ::Name& propertyId) const
+    {
+        if (!IsOpen())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document is not open.");
+            return m_invalidProperty;
+        }
+
+        const auto it = m_properties.find(propertyId);
+        if (it == m_properties.end())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document property could not be found: '%s'.", propertyId.GetCStr());
+            return m_invalidProperty;
+        }
+
+        const AtomToolsFramework::DynamicProperty& property = it->second;
+        return property;
+    }
+    
+    bool MaterialCanvasDocument::IsPropertyGroupVisible(const AZ::Name& propertyGroupFullName) const
+    {
+        if (!IsOpen())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document is not open.");
+            return false;
+        }
+
+        const auto it = m_propertyGroupVisibility.find(propertyGroupFullName);
+        if (it == m_propertyGroupVisibility.end())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document property group could not be found: '%s'.", propertyGroupFullName.GetCStr());
+            return false;
+        }
+
+        return it->second;
+    }
+
+    void MaterialCanvasDocument::SetPropertyValue(const AZ::Name& propertyId, [[maybe_unused]] const AZStd::any& value)
+    {
+        if (!IsOpen())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document is not open.");
+            return;
+        }
+
+        const auto it = m_properties.find(propertyId);
+        if (it == m_properties.end())
+        {
+            AZ_Error("MaterialCanvasDocument", false, "Document property could not be found: '%s'.", propertyId.GetCStr());
+            return;
+        }
+
+        AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(
+            &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentPropertyValueModified, m_id, it->second);
+        AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(
+            &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentModified, m_id);
+    }
+
+    bool MaterialCanvasDocument::IsModified() const
+    {
+        return AZStd::any_of(m_properties.begin(), m_properties.end(),
+            [](const auto& propertyPair)
+        {
+            const AtomToolsFramework::DynamicProperty& property = propertyPair.second;
+            return !AtomToolsFramework::ArePropertyValuesEqual(property.GetValue(), property.GetConfig().m_originalValue);
+        });
+    }
+
+    bool MaterialCanvasDocument::IsSavable() const
+    {
+        return true;
+    }
+
+    bool MaterialCanvasDocument::BeginEdit()
+    {
+        // Save the current properties as a momento for undo before any changes are applied
+        m_propertyValuesBeforeEdit.clear();
+        for (const auto& propertyPair : m_properties)
+        {
+            const AtomToolsFramework::DynamicProperty& property = propertyPair.second;
+            m_propertyValuesBeforeEdit[property.GetId()] = property.GetValue();
+        }
+        return true;
+    }
+
+    bool MaterialCanvasDocument::EndEdit()
+    {
+        PropertyValueMap propertyValuesForUndo;
+        PropertyValueMap propertyValuesForRedo;
+
+        // After editing has completed, check to see if properties have changed so the deltas can be recorded in the history
+        for (const auto& propertyBeforeEditPair : m_propertyValuesBeforeEdit)
+        {
+            const auto& propertyName = propertyBeforeEditPair.first;
+            const auto& propertyValueForUndo = propertyBeforeEditPair.second;
+            const auto& propertyValueForRedo = GetPropertyValue(propertyName);
+            if (!AtomToolsFramework::ArePropertyValuesEqual(propertyValueForUndo, propertyValueForRedo))
+            {
+                propertyValuesForUndo[propertyName] = propertyValueForUndo;
+                propertyValuesForRedo[propertyName] = propertyValueForRedo;
+            }
+        }
+
+        if (!propertyValuesForUndo.empty() && !propertyValuesForRedo.empty())
+        {
+            AddUndoRedoHistory(
+                [this, propertyValuesForUndo]() { RestorePropertyValues(propertyValuesForUndo); },
+                [this, propertyValuesForRedo]() { RestorePropertyValues(propertyValuesForRedo); });
+        }
+
+        m_propertyValuesBeforeEdit.clear();
+        return true;
+    }
+
+    void MaterialCanvasDocument::Clear()
+    {
+        AtomToolsFramework::AtomToolsDocument::Clear();
+
+        m_compilePending = {};
+        m_properties.clear();
+        m_propertyValuesBeforeEdit.clear();
+    }
+
+    void MaterialCanvasDocument::RestorePropertyValues(const PropertyValueMap& propertyValues)
+    {
+        for (const auto& propertyValuePair : propertyValues)
+        {
+            const auto& propertyName = propertyValuePair.first;
+            const auto& propertyValue = propertyValuePair.second;
+            SetPropertyValue(propertyName, propertyValue);
+        }
+    }
+
+} // namespace MaterialCanvas

+ 70 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocument.h

@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AtomToolsFramework/Document/AtomToolsDocument.h>
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/RTTI/RTTI.h>
+#include <Document/MaterialCanvasDocumentRequestBus.h>
+
+namespace MaterialCanvas
+{
+    //! MaterialCanvasDocument
+    class MaterialCanvasDocument
+        : public AtomToolsFramework::AtomToolsDocument
+        , public MaterialCanvasDocumentRequestBus::Handler
+    {
+    public:
+        AZ_RTTI(MaterialCanvasDocument, "{DBA269AE-892B-415C-8FA1-166B94B0E045}");
+        AZ_CLASS_ALLOCATOR(MaterialCanvasDocument, AZ::SystemAllocator, 0);
+        AZ_DISABLE_COPY(MaterialCanvasDocument);
+
+        MaterialCanvasDocument();
+        virtual ~MaterialCanvasDocument();
+
+        // AtomToolsFramework::AtomToolsDocument overrides...
+        const AZStd::any& GetPropertyValue(const AZ::Name& propertyId) const override;
+        const AtomToolsFramework::DynamicProperty& GetProperty(const AZ::Name& propertyId) const override;
+        bool IsPropertyGroupVisible(const AZ::Name& propertyGroupFullName) const override;
+        void SetPropertyValue(const AZ::Name& propertyId, const AZStd::any& value) override;
+        bool IsModified() const override;
+        bool IsSavable() const override;
+        bool BeginEdit() override;
+        bool EndEdit() override;
+
+    private:
+        // Predicate for evaluating properties
+        using PropertyFilterFunction = AZStd::function<bool(const AtomToolsFramework::DynamicProperty&)>;
+
+        // Map of document's properties
+        using PropertyMap = AZStd::unordered_map<AZ::Name, AtomToolsFramework::DynamicProperty>;
+
+        // Map of raw property values for undo/redo comparison and storage
+        using PropertyValueMap = AZStd::unordered_map<AZ::Name, AZStd::any>;
+
+        // Map of document's property group visibility flags
+        using PropertyGroupVisibilityMap = AZStd::unordered_map<AZ::Name, bool>;
+
+        // AtomToolsFramework::AtomToolsDocument overrides...
+        void Clear() override;
+
+        void RestorePropertyValues(const PropertyValueMap& propertyValues);
+
+        // If material instance value(s) were modified, do we need to recompile on next tick?
+        bool m_compilePending = false;
+
+        // Collection of all material's properties
+        PropertyMap m_properties;
+
+        // Collection of all material's property groups
+        PropertyGroupVisibilityMap m_propertyGroupVisibility;
+
+        // State of property values prior to an edit, used for restoration during undo
+        PropertyValueMap m_propertyValuesBeforeEdit;
+    };
+} // namespace MaterialCanvas

+ 22 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocumentRequestBus.h

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+namespace MaterialCanvas
+{
+    class MaterialCanvasDocumentRequests : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
+        static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
+        typedef AZ::Uuid BusIdType;
+    };
+
+    using MaterialCanvasDocumentRequestBus = AZ::EBus<MaterialCanvasDocumentRequests>;
+} // namespace MaterialCanvas

+ 47 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocumentSettings.cpp

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/RTTI/BehaviorContext.h>
+#include <AzCore/Serialization/EditContext.h>
+#include <Document/MaterialCanvasDocumentSettings.h>
+
+namespace MaterialCanvas
+{
+    void MaterialCanvasDocumentSettings::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MaterialCanvasDocumentSettings, AZ::UserSettings>()
+                ->Version(1)
+                ->Field("defaultMaterialTypeName", &MaterialCanvasDocumentSettings::m_defaultMaterialTypeName)
+            ;
+
+            if (auto editContext = serializeContext->GetEditContext())
+            {
+                editContext->Class<MaterialCanvasDocumentSettings>(
+                    "MaterialCanvasDocumentSettings", "")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &MaterialCanvasDocumentSettings::m_defaultMaterialTypeName, "Default Material Type Name", "")
+                    ;
+            }
+        }
+
+        if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->Class<MaterialCanvasDocumentSettings>("MaterialCanvasDocumentSettings")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Editor")
+                ->Attribute(AZ::Script::Attributes::Module, "materialcanvas")
+                ->Constructor()
+                ->Constructor<const MaterialCanvasDocumentSettings&>()
+                ->Property("defaultMaterialTypeName", BehaviorValueProperty(&MaterialCanvasDocumentSettings::m_defaultMaterialTypeName))
+                ;
+        }
+    }
+} // namespace MaterialCanvas

+ 30 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Document/MaterialCanvasDocumentSettings.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <AzCore/Memory/Memory.h>
+#include <AzCore/RTTI/RTTI.h>
+#include <AzCore/RTTI/ReflectContext.h>
+#include <AzCore/UserSettings/UserSettings.h>
+#endif
+
+namespace MaterialCanvas
+{
+    struct MaterialCanvasDocumentSettings
+        : public AZ::UserSettings
+    {
+        AZ_RTTI(MaterialCanvasDocumentSettings, "{12E8461F-65AD-4AD2-8A1D-82C3B1183522}", AZ::UserSettings);
+        AZ_CLASS_ALLOCATOR(MaterialCanvasDocumentSettings, AZ::SystemAllocator, 0);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        AZStd::string m_defaultMaterialTypeName = "StandardPBR";
+    };
+} // namespace MaterialCanvas

+ 127 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/MaterialCanvasApplication.cpp

@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AtomToolsFramework/Document/AtomToolsDocumentSystemRequestBus.h>
+#include <AzCore/RTTI/BehaviorContext.h>
+#include <AzCore/Serialization/EditContext.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+#include <AzToolsFramework/API/ToolsApplicationAPI.h>
+#include <Document/MaterialCanvasDocument.h>
+#include <Document/MaterialCanvasDocumentRequestBus.h>
+#include <Document/MaterialCanvasDocumentSettings.h>
+#include <MaterialCanvasApplication.h>
+#include <MaterialCanvas_Traits_Platform.h>
+#include <Viewport/MaterialCanvasViewportModule.h>
+#include <Window/MaterialCanvasMainWindow.h>
+#include <Window/MaterialCanvasMainWindowSettings.h>
+
+void InitMaterialCanvasResources()
+{
+    // Must register qt resources from other modules
+    Q_INIT_RESOURCE(MaterialCanvas);
+    Q_INIT_RESOURCE(InspectorWidget);
+    Q_INIT_RESOURCE(AtomToolsAssetBrowser);
+}
+
+namespace MaterialCanvas
+{
+    MaterialCanvasApplication::MaterialCanvasApplication(int* argc, char*** argv)
+        : Base(argc, argv)
+    {
+        InitMaterialCanvasResources();
+
+        QApplication::setApplicationName("O3DE Material Canvas");
+
+        // The settings registry has been created at this point, so add the CMake target
+        AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddBuildSystemTargetSpecialization(
+            *AZ::SettingsRegistry::Get(), GetBuildTargetName());
+
+        AzToolsFramework::EditorWindowRequestBus::Handler::BusConnect();
+        AtomToolsFramework::AtomToolsMainWindowFactoryRequestBus::Handler::BusConnect();
+    }
+
+    MaterialCanvasApplication::~MaterialCanvasApplication()
+    {
+        AtomToolsFramework::AtomToolsMainWindowFactoryRequestBus::Handler::BusDisconnect();
+        AzToolsFramework::EditorWindowRequestBus::Handler::BusDisconnect();
+        m_window.reset();
+    }
+
+    void MaterialCanvasApplication::Reflect(AZ::ReflectContext* context)
+    {
+        Base::Reflect(context);
+        MaterialCanvasDocumentSettings::Reflect(context);
+        MaterialCanvasMainWindowSettings::Reflect(context);
+
+        if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->EBus<MaterialCanvasDocumentRequestBus>("MaterialCanvasDocumentRequestBus")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Editor")
+                ->Attribute(AZ::Script::Attributes::Module, "materialcanvas")
+                ;
+        }
+    }
+
+    void MaterialCanvasApplication::CreateStaticModules(AZStd::vector<AZ::Module*>& outModules)
+    {
+        Base::CreateStaticModules(outModules);
+        outModules.push_back(aznew MaterialCanvasViewportModule);
+    }
+
+    const char* MaterialCanvasApplication::GetCurrentConfigurationName() const
+    {
+#if defined(_RELEASE)
+        return "ReleaseMaterialCanvas";
+#elif defined(_DEBUG)
+        return "DebugMaterialCanvas";
+#else
+        return "ProfileMaterialCanvas";
+#endif
+    }
+
+    void MaterialCanvasApplication::StartCommon(AZ::Entity* systemEntity)
+    {
+        Base::StartCommon(systemEntity);
+
+        AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Broadcast(
+            &AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Handler::RegisterDocumentType,
+            []() { return aznew MaterialCanvasDocument(); });
+    }
+
+    AZStd::string MaterialCanvasApplication::GetBuildTargetName() const
+    {
+#if !defined(LY_CMAKE_TARGET)
+#error "LY_CMAKE_TARGET must be defined in order to add this source file to a CMake executable target"
+#endif
+        //! Returns the build system target name of "MaterialCanvas"
+        return AZStd::string{ LY_CMAKE_TARGET };
+    }
+
+    AZStd::vector<AZStd::string> MaterialCanvasApplication::GetCriticalAssetFilters() const
+    {
+        return AZStd::vector<AZStd::string>({ "passes/", "config/", "MaterialCanvas/" });
+    }
+
+    void MaterialCanvasApplication::CreateMainWindow()
+    {
+        m_materialCanvasBrowserInteractions.reset(aznew MaterialCanvasBrowserInteractions);
+        m_window.reset(aznew MaterialCanvasMainWindow);
+    }
+
+    void MaterialCanvasApplication::DestroyMainWindow()
+    {
+        m_window.reset();
+    }
+
+    QWidget* MaterialCanvasApplication::GetAppMainWindow()
+    {
+        return m_window.get();
+    }
+} // namespace MaterialCanvas

+ 54 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/MaterialCanvasApplication.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AtomToolsFramework/Document/AtomToolsDocumentApplication.h>
+#include <AtomToolsFramework/Window/AtomToolsMainWindowFactoryRequestBus.h>
+#include <AzToolsFramework/API/EditorWindowRequestBus.h>
+#include <Window/MaterialCanvasBrowserInteractions.h>
+#include <Window/MaterialCanvasMainWindow.h>
+
+namespace MaterialCanvas
+{
+    class MaterialThumbnailRenderer;
+
+    class MaterialCanvasApplication
+        : public AtomToolsFramework::AtomToolsDocumentApplication
+        , private AzToolsFramework::EditorWindowRequestBus::Handler
+        , private AtomToolsFramework::AtomToolsMainWindowFactoryRequestBus::Handler
+    {
+    public:
+        AZ_TYPE_INFO(MaterialCanvas::MaterialCanvasApplication, "{30F90CA5-1253-49B5-8143-19CEE37E22BB}");
+
+        using Base = AtomToolsFramework::AtomToolsDocumentApplication;
+
+        MaterialCanvasApplication(int* argc, char*** argv);
+        ~MaterialCanvasApplication();
+
+        // AzFramework::Application overrides...
+        void Reflect(AZ::ReflectContext* context) override;
+        void CreateStaticModules(AZStd::vector<AZ::Module*>& outModules) override;
+        const char* GetCurrentConfigurationName() const override;
+        void StartCommon(AZ::Entity* systemEntity) override;
+
+        // AtomToolsFramework::AtomToolsApplication overrides...
+        AZStd::string GetBuildTargetName() const override;
+        AZStd::vector<AZStd::string> GetCriticalAssetFilters() const override;
+
+        // AtomToolsMainWindowFactoryRequestBus::Handler overrides...
+        void CreateMainWindow() override;
+        void DestroyMainWindow() override;
+
+        // AzToolsFramework::EditorWindowRequests::Bus::Handler
+        QWidget* GetAppMainWindow() override;
+
+        AZStd::unique_ptr<MaterialCanvasMainWindow> m_window;
+        AZStd::unique_ptr<MaterialCanvasBrowserInteractions> m_materialCanvasBrowserInteractions;
+    };
+} // namespace MaterialCanvas

+ 9 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Android/PAL_android.cmake

@@ -0,0 +1,9 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(PAL_TRAIT_ATOM_MATERIAL_CANVAS_APPLICATION_SUPPORTED FALSE)

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Android/platform_android_files.cmake

@@ -0,0 +1,10 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(FILES
+)

+ 8 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/MaterialCanvas_Traits_Linux.h

@@ -0,0 +1,8 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/MaterialCanvas_Traits_Platform.h

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <MaterialCanvas_Traits_Linux.h>

+ 9 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/PAL_linux.cmake

@@ -0,0 +1,9 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(PAL_TRAIT_ATOM_MATERIAL_CANVAS_APPLICATION_SUPPORTED TRUE)

+ 12 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/platform_linux_files.cmake

@@ -0,0 +1,12 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(FILES
+    MaterialCanvas_Traits_Platform.h
+    MaterialCanvas_Traits_Linux.h
+)

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Linux/tool_dependencies_linux.cmake

@@ -0,0 +1,10 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(LY_RUNTIME_DEPENDENCIES
+)

+ 8 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/MaterialCanvas_Traits_Mac.h

@@ -0,0 +1,8 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/MaterialCanvas_Traits_Platform.h

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <MaterialCanvas_Traits_Mac.h>

+ 9 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/PAL_mac.cmake

@@ -0,0 +1,9 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(PAL_TRAIT_ATOM_MATERIAL_CANVAS_APPLICATION_SUPPORTED TRUE)

+ 12 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/platform_mac_files.cmake

@@ -0,0 +1,12 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(FILES
+    MaterialCanvas_Traits_Platform.h
+    MaterialCanvas_Traits_Mac.h
+)

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Mac/tool_dependencies_mac.cmake

@@ -0,0 +1,10 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(LY_RUNTIME_DEPENDENCIES
+)

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas.ico

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:40dd06da9f9dcfef7536255fb53278c9dc4e7ee175d0395f655df6714912ce02
+size 109153

+ 1 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas.rc

@@ -0,0 +1 @@
+IDI_ICON1 ICON DISCARDABLE "MaterialCanvas.ico"

+ 14 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas_Traits_Platform.h

@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AzCore/PlatformIncl.h>
+#include <AzFramework/Input/Buses/Notifications/RawInputNotificationBus_Windows.h>
+#include <AzFramework/Windowing/WindowBus.h>
+
+#include <MaterialCanvas_Traits_Windows.h>

+ 8 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/MaterialCanvas_Traits_Windows.h

@@ -0,0 +1,8 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once

+ 9 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/PAL_windows.cmake

@@ -0,0 +1,9 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(PAL_TRAIT_ATOM_MATERIAL_CANVAS_APPLICATION_SUPPORTED TRUE)

+ 13 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/platform_windows_files.cmake

@@ -0,0 +1,13 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(FILES
+    MaterialCanvas_Traits_Platform.h
+    MaterialCanvas_Traits_Windows.h
+    MaterialCanvas.rc
+)

+ 11 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/Windows/tool_dependencies_windows.cmake

@@ -0,0 +1,11 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(LY_RUNTIME_DEPENDENCIES
+    Gem::QtForPython.Editor
+)

+ 9 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/iOS/PAL_ios.cmake

@@ -0,0 +1,9 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(PAL_TRAIT_ATOM_MATERIAL_CANVAS_APPLICATION_SUPPORTED FALSE)

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Platform/iOS/platform_ios_files.cmake

@@ -0,0 +1,10 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(FILES
+)

+ 166 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/Behavior.cpp

@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Math/Vector3.h>
+#include <Viewport/InputController/Behavior.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputController.h>
+
+namespace MaterialCanvas
+{
+    Behavior::Behavior()
+    {
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    Behavior::~Behavior()
+    {
+        AZ::TickBus::Handler::BusDisconnect();
+    }
+
+    void Behavior::Start()
+    {
+        m_x = 0;
+        m_y = 0;
+        m_z = 0;
+
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            m_cameraEntityId,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetCameraEntityId);
+        AZ_Assert(m_cameraEntityId.IsValid(), "Failed to find m_cameraEntityId");
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            m_distanceToTarget,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetDistanceToTarget);
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            m_targetPosition,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetTargetPosition);
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            m_radius, &MaterialCanvasViewportInputControllerRequestBus::Handler::GetRadius);
+    }
+
+    void Behavior::End()
+    {
+    }
+
+    void Behavior::MoveX(float value)
+    {
+        m_x += value * GetSensitivityX();
+    }
+
+    void Behavior::MoveY(float value)
+    {
+        m_y += value * GetSensitivityY();
+    }
+
+    void Behavior::MoveZ(float value)
+    {
+        m_z += value * GetSensitivityZ();
+    }
+
+    bool Behavior::HasDelta() const
+    {
+        return
+            AZ::GetAbs(m_x) > std::numeric_limits<float>::min() ||
+            AZ::GetAbs(m_y) > std::numeric_limits<float>::min() ||
+            AZ::GetAbs(m_z) > std::numeric_limits<float>::min();
+    }
+
+    void Behavior::TickInternal([[maybe_unused]] float x, [[maybe_unused]] float y, float z)
+    {
+        m_distanceToTarget = m_distanceToTarget - z;
+
+        bool isCameraCentered = false;
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            isCameraCentered,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::IsCameraCentered);
+
+        // if camera is looking at the model (locked to the model) we don't want to zoom past the model's center
+        if (isCameraCentered)
+        {
+            m_distanceToTarget = AZ::GetMax(m_distanceToTarget, 0.0f);
+        }
+
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        AZ::Vector3 position = m_targetPosition -
+            transform.GetRotation().TransformVector(AZ::Vector3::CreateAxisY(m_distanceToTarget));
+        AZ::TransformBus::Event(m_cameraEntityId, &AZ::TransformBus::Events::SetLocalTranslation, position);
+
+        // if camera is not locked to the model, move its focal point so we can free look
+        if (!isCameraCentered)
+        {
+            m_targetPosition += transform.GetRotation().TransformVector(AZ::Vector3::CreateAxisY(z));
+            MaterialCanvasViewportInputControllerRequestBus::Broadcast(
+                &MaterialCanvasViewportInputControllerRequestBus::Handler::SetTargetPosition,
+                m_targetPosition);
+            MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+                m_distanceToTarget,
+                &MaterialCanvasViewportInputControllerRequestBus::Handler::GetDistanceToTarget);
+        }
+    }
+
+    float Behavior::GetSensitivityX()
+    {
+        return 0;
+    }
+
+    float Behavior::GetSensitivityY()
+    {
+        return 0;
+    }
+
+    float Behavior::GetSensitivityZ()
+    {
+        // adjust zooming sensitivity by model size, so that large models zoom at the same speed as smaller ones
+        return 0.001f * AZ::GetMax(0.5f, m_radius);
+    }
+
+    AZ::Quaternion Behavior::LookRotation(AZ::Vector3 forward)
+    {
+        forward.Normalize();
+        AZ::Vector3 right = forward.CrossZAxis();
+        right.Normalize();
+        AZ::Vector3 up = right.Cross(forward);
+        up.Normalize();
+        AZ::Quaternion rotation = AZ::Quaternion::CreateFromBasis(right, forward, up);
+        rotation.Normalize();
+        return rotation;
+    }
+
+    float Behavior::TakeStep(float& value, float t)
+    {
+        const float absValue = AZ::GetAbs(value);
+        float step;
+        if (absValue < SnapInterval)
+        {
+            step = value;
+        }
+        else
+        {
+            step = AZ::Lerp(0, value, t);
+        }
+        value -= step;
+        return step;
+    }
+
+    void Behavior::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
+    {
+        // delta x and y values are accumulated in MoveX and MoveY functions (by dragging the mouse)
+        // in the Tick function we then lerp them down to 0 over short time and apply delta transform to an entity
+        if (HasDelta())
+        {
+            // t is a lerp amount based on time between frames (deltaTime)
+            // GetMin restricts how much we can lerp in case of low fps (and very high deltaTime)
+            const float t = AZ::GetMin(deltaTime / LerpTime, 0.5f);
+            const float x = TakeStep(m_x, t);
+            const float y = TakeStep(m_y, t);
+            const float z = TakeStep(m_z, t);
+            TickInternal(x, y, z);
+        }
+    }
+} // namespace MaterialCanvas

+ 64 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/Behavior.h

@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AzCore/Component/EntityId.h>
+#include <AzCore/Math/Quaternion.h>
+#include <AzCore/Component/TickBus.h>
+
+namespace MaterialCanvas
+{
+    //! Performs a single type of action for MaterialCanvasViewportInputController based on input
+    //! See derived behaviors for specific details
+    class Behavior
+        : public AZ::TickBus::Handler
+    {
+    public:
+        Behavior();
+        virtual ~Behavior();
+
+        virtual void Start();
+        virtual void End();
+        virtual void MoveX(float value);
+        virtual void MoveY(float value);
+        virtual void MoveZ(float value);
+
+    protected:
+        bool HasDelta() const;
+        virtual void TickInternal(float x, float y, float z);
+        virtual float GetSensitivityX();
+        virtual float GetSensitivityY();
+        virtual float GetSensitivityZ();
+
+        //! Calculate rotation quaternion towards a forward vector along world up axis
+        static AZ::Quaternion LookRotation(AZ::Vector3 forward);
+        //! Lerp a float value to 0 then decrement it by that interval and return it
+        static float TakeStep(float& value, float t);
+
+        //! Time in seconds to approximately complete a transformation
+        static constexpr float LerpTime = 0.05f;
+        //! If delta transform less than this, snap instantly
+        static constexpr float SnapInterval = 0.01f;
+        //! delta x movement accumulated during current frame
+        float m_x = 0;
+        //! delta y movement accumulated during current frame
+        float m_y = 0;
+        //! delta scroll wheel accumulated during current frame
+        float m_z = 0;
+        //! Model radius
+        float m_radius = 1.0f;
+
+        AZ::EntityId m_cameraEntityId;
+        AZ::Vector3 m_targetPosition = AZ::Vector3::CreateZero();
+        float m_distanceToTarget = 0;
+
+    private:
+        // AZ::TickBus::Handler interface overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+    };
+} // namespace MaterialCanvas

+ 36 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/DollyCameraBehavior.cpp

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Math/MathUtils.h>
+#include <AzCore/Math/Vector3.h>
+#include <Viewport/InputController/DollyCameraBehavior.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputController.h>
+
+namespace MaterialCanvas
+{
+    void DollyCameraBehavior::TickInternal([[maybe_unused]] float x, float y, [[maybe_unused]] float z)
+    {
+        m_distanceToTarget = m_distanceToTarget + y;
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        AZ::Vector3 position = m_targetPosition -
+            transform.GetRotation().TransformVector(AZ::Vector3::CreateAxisY(m_distanceToTarget));
+        AZ::TransformBus::Event(m_cameraEntityId, &AZ::TransformBus::Events::SetLocalTranslation, position);
+    }
+
+    float DollyCameraBehavior::GetSensitivityX()
+    {
+        return SensitivityX;
+    }
+
+    float DollyCameraBehavior::GetSensitivityY()
+    {
+        return SensitivityY;
+    }
+} // namespace MaterialCanvas

+ 30 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/DollyCameraBehavior.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <Viewport/InputController/Behavior.h>
+
+namespace MaterialCanvas
+{
+    //! Moves(zooms) camera back and forth towards the target
+    class DollyCameraBehavior final
+        : public Behavior
+    {
+    public:
+        DollyCameraBehavior() = default;
+        virtual ~DollyCameraBehavior() = default;
+    protected:
+        void TickInternal(float x, float y, float z) override;
+        float GetSensitivityX() override;
+        float GetSensitivityY() override;
+
+    private:
+        static constexpr float SensitivityX = 0;
+        static constexpr float SensitivityY = 0.005f;
+    };
+} // namespace MaterialCanvas

+ 13 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/IdleBehavior.cpp

@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Viewport/InputController/IdleBehavior.h>
+
+namespace MaterialCanvas
+{
+} // namespace MaterialCanvas

+ 22 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/IdleBehavior.h

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <Viewport/InputController/Behavior.h>
+
+namespace MaterialCanvas
+{
+    //! No action taken
+    class IdleBehavior final
+        : public Behavior
+    {
+    public:
+        IdleBehavior() = default;
+        virtual ~IdleBehavior() = default;
+    };
+} // namespace MaterialCanvas

+ 352 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MaterialCanvasViewportInputController.cpp

@@ -0,0 +1,352 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <QApplication>
+#include <QWidget>
+
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Math/Matrix4x4.h>
+#include <AzCore/Serialization/SerializeContext.h>
+
+#include <AzFramework/Components/CameraBus.h>
+#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
+#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
+#include <AzFramework/Viewport/ScreenGeometry.h>
+#include <AzToolsFramework/Viewport/ViewportMessages.h>
+
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h>
+
+#include <Viewport/InputController/DollyCameraBehavior.h>
+#include <Viewport/InputController/IdleBehavior.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputController.h>
+#include <Viewport/InputController/MoveCameraBehavior.h>
+#include <Viewport/InputController/OrbitCameraBehavior.h>
+#include <Viewport/InputController/PanCameraBehavior.h>
+#include <Viewport/InputController/RotateEnvironmentBehavior.h>
+#include <Viewport/InputController/RotateModelBehavior.h>
+
+namespace MaterialCanvas
+{
+    MaterialCanvasViewportInputController::MaterialCanvasViewportInputController()
+        : AzFramework::SingleViewportController()
+        , m_targetPosition(AZ::Vector3::CreateZero())
+    {
+        m_behaviorMap[None] = AZStd::make_shared<IdleBehavior>();
+        m_behaviorMap[Lmb] = AZStd::make_shared<PanCameraBehavior>();
+        m_behaviorMap[Mmb] = AZStd::make_shared<MoveCameraBehavior>();
+        m_behaviorMap[Rmb] = AZStd::make_shared<OrbitCameraBehavior>();
+        m_behaviorMap[Alt ^ Lmb] = AZStd::make_shared<OrbitCameraBehavior>();
+        m_behaviorMap[Alt ^ Mmb] = AZStd::make_shared<MoveCameraBehavior>();
+        m_behaviorMap[Alt ^ Rmb] = AZStd::make_shared<DollyCameraBehavior>();
+        m_behaviorMap[Lmb ^ Rmb] = AZStd::make_shared<DollyCameraBehavior>();
+        m_behaviorMap[Ctrl ^ Lmb] = AZStd::make_shared<RotateModelBehavior>();
+        m_behaviorMap[Shift ^ Lmb] = AZStd::make_shared<RotateEnvironmentBehavior>();
+    }
+
+    MaterialCanvasViewportInputController::~MaterialCanvasViewportInputController()
+    {
+        if (m_initialized)
+        {
+            MaterialCanvasViewportInputControllerRequestBus::Handler::BusDisconnect();
+        }
+    }
+
+    void MaterialCanvasViewportInputController::Init(const AZ::EntityId& cameraEntityId, const AZ::EntityId& targetEntityId, const AZ::EntityId& iblEntityId)
+    {
+        if (m_initialized)
+        {
+            AZ_Error("MaterialCanvasViewportInputController", false, "Controller already initialized.");
+            return;
+        }
+        m_initialized = true;
+        m_cameraEntityId = cameraEntityId;
+        m_targetEntityId = targetEntityId;
+        m_iblEntityId = iblEntityId;
+
+        MaterialCanvasViewportInputControllerRequestBus::Handler::BusConnect();
+    }
+
+    const AZ::EntityId& MaterialCanvasViewportInputController::GetCameraEntityId() const
+    {
+        return m_cameraEntityId;
+    }
+
+    const AZ::EntityId& MaterialCanvasViewportInputController::GetTargetEntityId() const
+    {
+        return m_targetEntityId;
+    }
+
+    const AZ::EntityId& MaterialCanvasViewportInputController::GetIblEntityId() const
+    {
+        return m_iblEntityId;
+    }
+
+    const AZ::Vector3& MaterialCanvasViewportInputController::GetTargetPosition() const
+    {
+        return m_targetPosition;
+    }
+
+    void MaterialCanvasViewportInputController::SetTargetPosition(const AZ::Vector3& targetPosition)
+    {
+        m_targetPosition = targetPosition;
+        m_isCameraCentered = false;
+    }
+
+    float MaterialCanvasViewportInputController::GetDistanceToTarget() const
+    {
+        AZ::Vector3 cameraPosition;
+        AZ::TransformBus::EventResult(cameraPosition, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTranslation);
+        return cameraPosition.GetDistance(m_targetPosition);
+    }
+
+    void MaterialCanvasViewportInputController::GetExtents(float& distanceMin, float& distanceMax) const
+    {
+        distanceMin = m_distanceMin;
+        distanceMax = m_distanceMax;
+    }
+
+    float MaterialCanvasViewportInputController::GetRadius() const
+    {
+        return m_radius;
+    }
+
+    void MaterialCanvasViewportInputController::UpdateViewport(const AzFramework::ViewportControllerUpdateEvent& event)
+    {
+        if (m_keysChanged)
+        {
+            if (m_timeToBehaviorSwitchMs > 0)
+            {
+                m_timeToBehaviorSwitchMs -= event.m_deltaTime.count();
+            }
+            if (m_timeToBehaviorSwitchMs <= 0)
+            {
+                EvaluateControlBehavior();
+                m_keysChanged = false;
+            }
+        }
+    }
+
+    bool MaterialCanvasViewportInputController::HandleInputChannelEvent(const AzFramework::ViewportControllerInputEvent& event)
+    {
+        using namespace AzFramework;
+
+        const InputChannelId& inputChannelId = event.m_inputChannel.GetInputChannelId();
+        const InputChannel::State state = event.m_inputChannel.GetState();
+        const KeyMask keysOld = m_keys;
+
+        bool mouseOver = false;
+        AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::EventResult(
+            mouseOver, GetViewportId(),
+            &AzToolsFramework::ViewportInteraction::ViewportMouseCursorRequestBus::Events::IsMouseOver);
+
+        if (!m_behavior)
+        {
+            EvaluateControlBehavior();
+        }
+
+        switch (state)
+        {
+        case InputChannel::State::Began:
+            if (inputChannelId == InputDeviceMouse::Button::Left)
+            {
+                m_keys |= Lmb;
+            }
+            else if (inputChannelId == InputDeviceMouse::Button::Middle)
+            {
+                m_keys |= Mmb;
+            }
+            else if (inputChannelId == InputDeviceMouse::Button::Right)
+            {
+                m_keys |= Rmb;
+            }
+            else if (inputChannelId == InputDeviceKeyboard::Key::ModifierAltL)
+            {
+                m_keys |= Alt;
+            }
+            else if (inputChannelId == InputDeviceKeyboard::Key::ModifierCtrlL)
+            {
+                m_keys |= Ctrl;
+            }
+            else if (inputChannelId == InputDeviceKeyboard::Key::ModifierShiftL)
+            {
+                m_keys |= Shift;
+            }
+            if (inputChannelId == InputDeviceMouse::Movement::X)
+            {
+                m_behavior->MoveX(event.m_inputChannel.GetValue());
+            }
+            else if (inputChannelId == InputDeviceMouse::Movement::Y)
+            {
+                m_behavior->MoveY(event.m_inputChannel.GetValue());
+            }
+            else if (inputChannelId == InputDeviceMouse::Movement::Z)
+            {
+                if (mouseOver)
+                {
+                    m_behavior->MoveZ(event.m_inputChannel.GetValue());
+                }
+            }
+            break;
+        case InputChannel::State::Ended:
+            if (inputChannelId == InputDeviceMouse::Button::Left)
+            {
+                m_keys &= ~Lmb;
+            }
+            else if (inputChannelId == InputDeviceMouse::Button::Middle)
+            {
+                m_keys &= ~Mmb;
+            }
+            else if (inputChannelId == InputDeviceMouse::Button::Right)
+            {
+                m_keys &= ~Rmb;
+            }
+            else if (inputChannelId == InputDeviceKeyboard::Key::ModifierAltL)
+            {
+                m_keys &= ~Alt;
+            }
+            else if (inputChannelId == InputDeviceKeyboard::Key::ModifierCtrlL)
+            {
+                m_keys &= ~Ctrl;
+            }
+            else if (inputChannelId == InputDeviceKeyboard::Key::ModifierShiftL)
+            {
+                m_keys &= ~Shift;
+            }
+            else if (inputChannelId == InputDeviceKeyboard::Key::AlphanumericZ && (m_keys & Ctrl) == None)
+            {
+                // only reset camera if no other widget besides viewport is in focus
+                const auto focus = QApplication::focusWidget();
+                if (!focus || focus->objectName() == "Viewport")
+                {
+                    Reset();
+                }
+            }
+            break;
+        case InputChannel::State::Updated:
+            if (inputChannelId == InputDeviceMouse::Movement::X)
+            {
+                m_behavior->MoveX(event.m_inputChannel.GetValue());
+            }
+            else if (inputChannelId == InputDeviceMouse::Movement::Y)
+            {
+                m_behavior->MoveY(event.m_inputChannel.GetValue());
+            }
+            else if (inputChannelId == InputDeviceMouse::Movement::Z)
+            {
+                if (mouseOver)
+                {
+                    m_behavior->MoveZ(event.m_inputChannel.GetValue());
+                }
+            }
+            break;
+        }
+
+        if (keysOld != m_keys)
+        {
+            m_keysChanged = true;
+            m_timeToBehaviorSwitchMs = BehaviorSwitchDelayMs;
+        }
+        return false;
+    }
+
+    void MaterialCanvasViewportInputController::Reset()
+    {
+        CalculateExtents();
+
+        // reset camera
+        m_targetPosition = m_modelCenter;
+        const float distance = m_distanceMin * StartingDistanceMultiplier;
+        const AZ::Quaternion cameraRotation = AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisZ(), StartingRotationAngle);
+        AZ::Vector3 cameraPosition(m_targetPosition.GetX(), m_targetPosition.GetY() - distance, m_targetPosition.GetZ());
+        cameraPosition = cameraRotation.TransformVector(cameraPosition);
+        AZ::Transform cameraTransform = AZ::Transform::CreateFromQuaternionAndTranslation(cameraRotation, cameraPosition);
+        AZ::TransformBus::Event(m_cameraEntityId, &AZ::TransformBus::Events::SetLocalTM, cameraTransform);
+        m_isCameraCentered = true;
+
+        // reset model
+        AZ::Transform modelTransform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::Event(m_targetEntityId, &AZ::TransformBus::Events::SetLocalTM, modelTransform);
+
+        // reset environment
+        AZ::Transform iblTransform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::Event(m_iblEntityId, &AZ::TransformBus::Events::SetLocalTM, iblTransform);
+
+        const AZ::Matrix4x4 rotationMatrix = AZ::Matrix4x4::CreateIdentity();
+        auto skyBoxFeatureProcessorInterface = AZ::RPI::Scene::GetFeatureProcessorForEntity<AZ::Render::SkyBoxFeatureProcessorInterface>(m_iblEntityId);
+        skyBoxFeatureProcessorInterface->SetCubemapRotationMatrix(rotationMatrix);
+
+        if (m_behavior)
+        {
+            m_behavior->End();
+            m_behavior->Start();
+        }
+    }
+
+    void MaterialCanvasViewportInputController::SetFieldOfView(float value)
+    {
+        Camera::CameraRequestBus::Event(m_cameraEntityId, &Camera::CameraRequestBus::Events::SetFovDegrees, value);
+    }
+
+    bool MaterialCanvasViewportInputController::IsCameraCentered() const
+    {
+        return m_isCameraCentered;
+    }
+
+    void MaterialCanvasViewportInputController::CalculateExtents()
+    {
+        AZ::TransformBus::EventResult(m_modelCenter, m_targetEntityId, &AZ::TransformBus::Events::GetLocalTranslation);
+
+        AZ::Data::AssetId modelAssetId;
+        AZ::Render::MeshComponentRequestBus::EventResult(modelAssetId, m_targetEntityId,
+            &AZ::Render::MeshComponentRequestBus::Events::GetModelAssetId);
+
+        if (modelAssetId.IsValid())
+        {
+            AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset = AZ::Data::AssetManager::Instance().GetAsset(modelAssetId, azrtti_typeid<AZ::RPI::ModelAsset>(), AZ::Data::AssetLoadBehavior::PreLoad);
+            modelAsset.BlockUntilLoadComplete();
+            if (modelAsset.IsReady())
+            {
+                const AZ::Aabb& aabb = modelAsset->GetAabb();
+                aabb.GetAsSphere(m_modelCenter, m_radius);
+
+                m_distanceMin = 0.5f * AZ::GetMin(AZ::GetMin(aabb.GetExtents().GetX(), aabb.GetExtents().GetY()), aabb.GetExtents().GetZ()) + DepthNear;
+                m_distanceMax = m_radius * MaxDistanceMultiplier;
+            }
+        }
+    }
+
+    void MaterialCanvasViewportInputController::EvaluateControlBehavior()
+    {
+        AZStd::shared_ptr<Behavior> nextBehavior;
+        auto it = m_behaviorMap.find(m_keys);
+        if (it == m_behaviorMap.end())
+        {
+            nextBehavior = m_behaviorMap[None];
+        }
+        else
+        {
+            nextBehavior = it->second;
+        }
+
+        if (nextBehavior == m_behavior)
+        {
+            return;
+        }
+
+        if (m_behavior)
+        {
+            m_behavior->End();
+        }
+        m_behavior = nextBehavior;
+        m_behavior->Start();
+    }
+} // namespace MaterialCanvas

+ 109 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MaterialCanvasViewportInputController.h

@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AzFramework/Input/Events/InputChannelEventListener.h>
+#include <AzFramework/Viewport/SingleViewportController.h>
+#include <Viewport/InputController/Behavior.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputControllerBus.h>
+
+namespace MaterialCanvas
+{
+    class Behavior;
+
+    //! Provides controls for manipulating camera, model, and environment
+    class MaterialCanvasViewportInputController
+        : public AzFramework::SingleViewportController
+        , public MaterialCanvasViewportInputControllerRequestBus::Handler
+    {
+    public:
+
+        AZ_TYPE_INFO(MaterialCanvasViewportInputController, "{569A0544-7654-4DCE-8156-00A71B408374}");
+        AZ_CLASS_ALLOCATOR(MaterialCanvasViewportInputController, AZ::SystemAllocator, 0)
+
+        MaterialCanvasViewportInputController();
+        virtual ~MaterialCanvasViewportInputController();
+
+        void Init(const AZ::EntityId& cameraEntityId, const AZ::EntityId& targetEntityId, const AZ::EntityId& iblEntityId);
+
+        // MaterialCanvasViewportInputControllerRequestBus::Handler interface overrides...
+        const AZ::EntityId& GetCameraEntityId() const override;
+        const AZ::EntityId& GetTargetEntityId() const override;
+        const AZ::EntityId& GetIblEntityId() const override;
+        const AZ::Vector3& GetTargetPosition() const override;
+        void SetTargetPosition(const AZ::Vector3& targetPosition) override;
+        float GetDistanceToTarget() const override;
+        void GetExtents(float& distanceMin, float& distanceMax) const override;
+        float GetRadius() const override;
+        void Reset() override;
+        void SetFieldOfView(float value) override;
+        bool IsCameraCentered() const override;
+
+        // AzFramework::ViewportControllerInstance interface overrides...
+        bool HandleInputChannelEvent(const AzFramework::ViewportControllerInputEvent& event) override;
+        void UpdateViewport(const AzFramework::ViewportControllerUpdateEvent& event) override;
+
+    private:
+        using KeyMask = uint32_t;
+
+        enum Keys
+        {
+            None = 0,
+            Lmb = 1 << 0,
+            Mmb = 1 << 1,
+            Rmb = 1 << 2,
+            Alt = 1 << 3,
+            Ctrl = 1 << 4,
+            Shift = 1 << 5
+        };
+
+        //! Calculate min and max dist and center based on mesh size of target model
+        void CalculateExtents();
+        //! Determine which behavior to set based on mouse/keyboard input
+        void EvaluateControlBehavior();
+
+        bool m_initialized = false;
+
+        //! Input keys currently pressed
+        KeyMask m_keys = None;
+        //! Input key sequence changed
+        bool m_keysChanged = false;
+        //! Time remaining before behavior switch
+        float m_timeToBehaviorSwitchMs = 0;
+
+        //! Current behavior of the controller
+        AZStd::shared_ptr<Behavior> m_behavior;
+        AZStd::unordered_map<KeyMask, AZStd::shared_ptr<Behavior>> m_behaviorMap;
+
+        AZ::EntityId m_cameraEntityId;
+        //! Target entity is looking at
+        AZ::EntityId m_targetEntityId;
+        //! IBL entity for rotating environment lighting
+        AZ::EntityId m_iblEntityId;
+        //! Target position camera is pointed towards
+        AZ::Vector3 m_targetPosition;
+        //! Center of the model observed
+        AZ::Vector3 m_modelCenter;
+        //! Minimum distance from camera to target
+        float m_distanceMin = 1.0f;
+        //! Maximum distance from camera to target
+        float m_distanceMax = 10.0f;
+        //! Model radius
+        float m_radius = 1.0f;
+        //! True if camera is centered on a model
+        bool m_isCameraCentered = true;
+
+        static constexpr float MaxDistanceMultiplier = 2.5f;
+        static constexpr float StartingDistanceMultiplier = 2.0f;
+        static constexpr float StartingRotationAngle = AZ::Constants::QuarterPi / 2.0f;
+        static constexpr float DepthNear = 0.01f;
+        //! Artificial delay between behavior switching to avoid switching into undesired behaviors with smaller key sequences
+        //! e.g. pressing RMB+LMB shouldn't switch into RMB behavior (or LMB behavior) first because it's virtually impossible to press both mouse buttons on the same frame
+        static constexpr float BehaviorSwitchDelayMs = 0.1f;
+    };
+} // namespace MaterialCanvas

+ 61 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MaterialCanvasViewportInputControllerBus.h

@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AzCore/EBus/EBus.h>
+#include <AzCore/Math/Vector3.h>
+
+namespace MaterialCanvas
+{
+    class MaterialCanvasViewportInputControllerRequests
+        : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
+        static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
+
+        //! Get entityId of viewport camera
+        virtual const AZ::EntityId& GetCameraEntityId() const = 0;
+
+        //! Get entityId of camera target
+        virtual const AZ::EntityId& GetTargetEntityId() const = 0;
+
+        //! Get entityId of scene's IBL entity
+        virtual const AZ::EntityId& GetIblEntityId() const = 0;
+
+        //! Get actual position where the camera is facing
+        virtual const AZ::Vector3& GetTargetPosition() const = 0;
+
+        //! Set camera target position
+        //! @param targetPosition world space position to point camera at
+        virtual void SetTargetPosition(const AZ::Vector3& targetPosition) = 0;
+
+        //! Get distance between camera and its target
+        virtual float GetDistanceToTarget() const = 0;
+
+        //! Get minimum and maximum camera distance to the model based on mesh size
+        //! @param distanceMin closest camera can be to the target
+        //! @param distanceMax furthest camera can be from the target
+        virtual void GetExtents(float& distanceMin, float& distanceMax) const = 0;
+
+        //! Get bounding sphere radius of the active model
+        virtual float GetRadius() const = 0;
+
+        //! Reset camera to default position and rotation 
+        virtual void Reset() = 0;
+
+        //! Modify camera's field of view
+        //! @param value field of view in degrees
+        virtual void SetFieldOfView(float value) = 0;
+
+        //! Check if camera is looking directly at a model
+        virtual bool IsCameraCentered() const = 0;
+    };
+
+    using MaterialCanvasViewportInputControllerRequestBus = AZ::EBus<MaterialCanvasViewportInputControllerRequests>;
+} // namespace MaterialCanvas

+ 56 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MoveCameraBehavior.cpp

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Math/Vector3.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputControllerBus.h>
+#include <Viewport/InputController/MoveCameraBehavior.h>
+
+namespace MaterialCanvas
+{
+    void MoveCameraBehavior::End()
+    {
+        float distanceToTarget;
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            distanceToTarget,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetDistanceToTarget);
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        AZ::Vector3 targetPosition =
+            transform.GetTranslation() +
+            transform.GetBasisY() * distanceToTarget;
+        MaterialCanvasViewportInputControllerRequestBus::Broadcast(
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::SetTargetPosition,
+            targetPosition);
+    }
+
+    void MoveCameraBehavior::TickInternal(float x, float y, float z)
+    {
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        AZ::Vector3 up = transform.GetBasisZ();
+        AZ::Vector3 right = transform.GetBasisX();
+        AZ::Vector3 position = transform.GetTranslation();
+        AZ::Vector3 deltaPosition = up * y + right * -x;
+        position += deltaPosition;
+        m_targetPosition += deltaPosition;
+        AZ::TransformBus::Event(m_cameraEntityId, &AZ::TransformBus::Events::SetLocalTranslation, position);
+
+        Behavior::TickInternal(x, y, z);
+    }
+
+    float MoveCameraBehavior::GetSensitivityX()
+    {
+        return SensitivityX;
+    }
+
+    float MoveCameraBehavior::GetSensitivityY()
+    {
+        return SensitivityY;
+    }
+} // namespace MaterialCanvas

+ 32 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/MoveCameraBehavior.h

@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <Viewport/InputController/Behavior.h>
+
+namespace MaterialCanvas
+{
+    //! Moves camera along its vertical and horizontal axis
+    class MoveCameraBehavior final
+        : public Behavior
+    {
+    public:
+        MoveCameraBehavior() = default;
+        virtual ~MoveCameraBehavior() = default;
+        void End() override;
+
+    protected:
+        void TickInternal(float x, float y, float z) override;
+        float GetSensitivityX() override;
+        float GetSensitivityY() override;
+
+    private:
+        static constexpr float SensitivityX = 0.01f;
+        static constexpr float SensitivityY = 0.01f;
+    };
+} // namespace MaterialCanvas

+ 59 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/OrbitCameraBehavior.cpp

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Component/TransformBus.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputController.h>
+#include <Viewport/InputController/OrbitCameraBehavior.h>
+
+namespace MaterialCanvas
+{
+    void OrbitCameraBehavior::TickInternal(float x, float y, float z)
+    {
+        Behavior::TickInternal(x, y, z);
+
+        // don't align camera until a movement has been made so that accidental right-click doesn't reset camera
+        if (!m_aligned)
+        {
+            Align();
+        }
+
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        AZ::Quaternion rotation = transform.GetRotation();
+        AZ::Vector3 right = transform.GetBasisX();
+        rotation =
+            AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisZ(), -x) *
+            AZ::Quaternion::CreateFromAxisAngle(right, -y) *
+            rotation;
+        rotation.Normalize();
+        AZ::Vector3 position =
+            rotation.TransformVector(AZ::Vector3(0, -m_distanceToTarget, 0)) + m_targetPosition;
+        transform = AZ::Transform::CreateFromQuaternionAndTranslation(rotation, position);
+        AZ::TransformBus::Event(m_cameraEntityId, &AZ::TransformBus::Events::SetLocalTM, transform);
+    }
+
+    float OrbitCameraBehavior::GetSensitivityX()
+    {
+        return SensitivityX;
+    }
+
+    float OrbitCameraBehavior::GetSensitivityY()
+    {
+        return SensitivityY;
+    }
+
+    void OrbitCameraBehavior::Align()
+    {
+        AZ::Vector3 cameraPosition = AZ::Vector3::CreateZero();
+        AZ::TransformBus::EventResult(cameraPosition, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTranslation);
+        const AZ::Vector3 delta = m_targetPosition - cameraPosition;
+        AZ::Quaternion targetRotation = LookRotation(delta);
+        AZ::TransformBus::Event(m_cameraEntityId, &AZ::TransformBus::Events::SetLocalRotationQuaternion, targetRotation);
+        m_aligned = true;
+    }
+} // namespace MaterialCanvas

+ 36 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/OrbitCameraBehavior.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <Viewport/InputController/Behavior.h>
+
+namespace MaterialCanvas
+{
+    //! Rotates the camera around target position,
+    //! this can either be model center or any position in world
+    class OrbitCameraBehavior final
+        : public Behavior
+    {
+    public:
+        OrbitCameraBehavior() = default;
+        virtual ~OrbitCameraBehavior() = default;
+
+    protected:
+        void TickInternal(float x, float y, float z) override;
+        float GetSensitivityX() override;
+        float GetSensitivityY() override;
+
+    private:
+        void Align();
+
+        static constexpr float SensitivityX = 0.005f;
+        static constexpr float SensitivityY = 0.005f;
+        
+        bool m_aligned = false;
+    };
+} // namespace MaterialCanvas

+ 56 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/PanCameraBehavior.cpp

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Math/Quaternion.h>
+#include <AzCore/Math/Vector3.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputControllerBus.h>
+#include <Viewport/InputController/PanCameraBehavior.h>
+
+namespace MaterialCanvas
+{
+    void PanCameraBehavior::End()
+    {
+        float distanceToTarget;
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            distanceToTarget,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetDistanceToTarget);
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        AZ::Vector3 targetPosition =
+            transform.GetTranslation() +
+            transform.GetBasisY() * distanceToTarget;
+        MaterialCanvasViewportInputControllerRequestBus::Broadcast(
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::SetTargetPosition,
+            targetPosition);
+    }
+
+    void PanCameraBehavior::TickInternal(float x, float y, [[maybe_unused]] float z)
+    {
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        AZ::Quaternion rotation = transform.GetRotation();
+        const AZ::Vector3 right = transform.GetBasisX();
+        rotation =
+            AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisZ(), -x) *
+            AZ::Quaternion::CreateFromAxisAngle(right, -y) *
+            rotation;
+        rotation.Normalize();
+        AZ::TransformBus::Event(m_cameraEntityId, &AZ::TransformBus::Events::SetLocalRotationQuaternion, rotation);
+    }
+
+    float PanCameraBehavior::GetSensitivityX()
+    {
+        return SensitivityX;
+    }
+
+    float PanCameraBehavior::GetSensitivityY()
+    {
+        return SensitivityY;
+    }
+} // namespace MaterialCanvas

+ 33 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/PanCameraBehavior.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <Viewport/InputController/Behavior.h>
+
+namespace MaterialCanvas
+{
+    //! Rotates camera around its own axis, allowing to look up/down/left/right
+    class PanCameraBehavior final
+        : public Behavior
+    {
+    public:
+        PanCameraBehavior() = default;
+        virtual ~PanCameraBehavior() = default;
+        
+        void End() override;
+
+    protected:
+        void TickInternal(float x, float y, float z) override;
+        float GetSensitivityX() override;
+        float GetSensitivityY() override;
+
+    private:
+        static constexpr float SensitivityX = 0.005f;
+        static constexpr float SensitivityY = 0.005f;
+    };
+} // namespace MaterialCanvas

+ 51 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateEnvironmentBehavior.cpp

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Component/TransformBus.h>
+
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Scene.h>
+
+#include <Viewport/InputController/MaterialCanvasViewportInputController.h>
+#include <Viewport/InputController/RotateEnvironmentBehavior.h>
+
+namespace MaterialCanvas
+{
+    void RotateEnvironmentBehavior::Start()
+    {
+        Behavior::Start();
+
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            m_iblEntityId,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetIblEntityId);
+        AZ_Assert(m_iblEntityId.IsValid(), "Failed to find m_iblEntityId");
+        m_skyBoxFeatureProcessorInterface = AZ::RPI::Scene::GetFeatureProcessorForEntity<AZ::Render::SkyBoxFeatureProcessorInterface>(m_iblEntityId);
+    }
+
+    void RotateEnvironmentBehavior::TickInternal(float x, float y, float z)
+    {
+        Behavior::TickInternal(x, y, z);
+
+        m_rotation += x;
+        AZ::Quaternion rotation = AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisZ(), m_rotation);
+        AZ::TransformBus::Event(m_iblEntityId, &AZ::TransformBus::Events::SetLocalRotationQuaternion, rotation);
+        const AZ::Matrix4x4 rotationMatrix = AZ::Matrix4x4::CreateFromQuaternion(rotation);
+        m_skyBoxFeatureProcessorInterface->SetCubemapRotationMatrix(rotationMatrix);
+    }
+
+    float RotateEnvironmentBehavior::GetSensitivityX()
+    {
+        return SensitivityX;
+    }
+
+    float RotateEnvironmentBehavior::GetSensitivityY()
+    {
+        return SensitivityY;
+    }
+} // namespace MaterialCanvas

+ 45 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateEnvironmentBehavior.h

@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <Viewport/InputController/Behavior.h>
+
+namespace AZ
+{
+    namespace Render
+    {
+        class SkyBoxFeatureProcessorInterface;
+    }
+}
+
+namespace MaterialCanvas
+{
+    //! Rotates lighting and skybox around vertical axis
+    class RotateEnvironmentBehavior final
+        : public Behavior
+    {
+    public:
+        RotateEnvironmentBehavior() = default;
+        virtual ~RotateEnvironmentBehavior() = default;
+
+        void Start() override;
+
+    protected:
+        void TickInternal(float x, float y, float z) override;
+        float GetSensitivityX() override;
+        float GetSensitivityY() override;
+
+    private:
+        static constexpr float SensitivityX = 0.01f;
+        static constexpr float SensitivityY = 0;
+
+        AZ::EntityId m_iblEntityId;
+        AZ::Render::SkyBoxFeatureProcessorInterface* m_skyBoxFeatureProcessorInterface = nullptr;
+        float m_rotation = 0;
+    };
+} // namespace MaterialCanvas

+ 59 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateModelBehavior.cpp

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Component/TransformBus.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputController.h>
+#include <Viewport/InputController/RotateModelBehavior.h>
+
+namespace MaterialCanvas
+{
+    void RotateModelBehavior::Start()
+    {
+        Behavior::Start();
+
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            m_targetEntityId,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetTargetEntityId);
+        AZ_Assert(m_targetEntityId.IsValid(), "Failed to find m_targetEntityId");
+        AZ::EntityId cameraEntityId;
+        MaterialCanvasViewportInputControllerRequestBus::BroadcastResult(
+            cameraEntityId,
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::GetCameraEntityId);
+        AZ_Assert(cameraEntityId.IsValid(), "Failed to find cameraEntityId");
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, cameraEntityId, &AZ::TransformBus::Events::GetLocalTM);
+        m_cameraRight = transform.GetBasisX();
+    }
+
+    void RotateModelBehavior::TickInternal(float x, float y, float z)
+    {
+        Behavior::TickInternal(x, y, z);
+
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        AZ::TransformBus::EventResult(transform, m_targetEntityId, &AZ::TransformBus::Events::GetLocalTM);
+
+        AZ::Quaternion rotation = transform.GetRotation();
+        rotation =
+            AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisZ(), x) *
+            AZ::Quaternion::CreateFromAxisAngle(m_cameraRight, y) *
+            rotation;
+        rotation.Normalize();
+
+        AZ::TransformBus::Event(m_targetEntityId, &AZ::TransformBus::Events::SetLocalRotationQuaternion, rotation);
+    }
+
+    float RotateModelBehavior::GetSensitivityX()
+    {
+        return SensitivityX;
+    }
+
+    float RotateModelBehavior::GetSensitivityY()
+    {
+        return SensitivityY;
+    }
+} // namespace MaterialCanvas

+ 36 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/InputController/RotateModelBehavior.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <Viewport/InputController/Behavior.h>
+
+namespace MaterialCanvas
+{
+    //! Rotates target model in viewport
+    class RotateModelBehavior final
+        : public Behavior
+    {
+    public:
+        RotateModelBehavior() = default;
+        virtual ~RotateModelBehavior() = default;
+
+        void Start() override;
+
+    protected:
+        void TickInternal(float x, float y, float z) override;
+        float GetSensitivityX() override;
+        float GetSensitivityY() override;
+
+    private:
+        static constexpr float SensitivityX = 0.01f;
+        static constexpr float SensitivityY = 0.01f;
+
+        AZ::EntityId m_targetEntityId;
+        AZ::Vector3 m_cameraRight = AZ::Vector3::CreateAxisX();
+    };
+} // namespace MaterialCanvas

+ 482 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportComponent.cpp

@@ -0,0 +1,482 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/RPI.Edit/Common/AssetUtils.h>
+#include <Atom/RPI.Edit/Common/JsonUtils.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/RTTI/BehaviorContext.h>
+#include <AzCore/Serialization/EditContext.h>
+#include <AzCore/Serialization/Json/JsonUtils.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/StringFunc/StringFunc.h>
+#include <AzFramework/Asset/AssetSystemBus.h>
+#include <AzFramework/IO/LocalFileIO.h>
+#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
+#include <AzToolsFramework/AssetBrowser/AssetBrowserEntry.h>
+#include <Viewport/MaterialCanvasViewportComponent.h>
+#include <Viewport/MaterialCanvasViewportNotificationBus.h>
+#include <Viewport/MaterialCanvasViewportSettings.h>
+
+namespace MaterialCanvas
+{
+    MaterialCanvasViewportComponent::MaterialCanvasViewportComponent()
+    {
+    }
+
+    void MaterialCanvasViewportComponent::Reflect(AZ::ReflectContext* context)
+    {
+        MaterialCanvasViewportSettings::Reflect(context);
+
+        if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serialize->Class<MaterialCanvasViewportComponent, AZ::Component>()
+                ->Version(0);
+
+            if (AZ::EditContext* editContext = serialize->GetEditContext())
+            {
+                editContext->Class<MaterialCanvasViewportComponent>("MaterialCanvasViewport", "Manages configurations for lighting and models displayed in the viewport")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System"))
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ;
+            }
+        }
+
+        if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->EBus<MaterialCanvasViewportRequestBus>("MaterialCanvasViewportRequestBus")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Editor")
+                ->Attribute(AZ::Script::Attributes::Module, "materialcanvas")
+                ->Event("ReloadContent", &MaterialCanvasViewportRequestBus::Events::ReloadContent)
+                ->Event("AddLightingPreset", &MaterialCanvasViewportRequestBus::Events::AddLightingPreset)
+                ->Event("SaveLightingPreset", &MaterialCanvasViewportRequestBus::Events::SaveLightingPreset)
+                ->Event("GetLightingPresets", &MaterialCanvasViewportRequestBus::Events::GetLightingPresets)
+                ->Event("GetLightingPresetByName", &MaterialCanvasViewportRequestBus::Events::GetLightingPresetByName)
+                ->Event("GetLightingPresetSelection", &MaterialCanvasViewportRequestBus::Events::GetLightingPresetSelection)
+                ->Event("SelectLightingPreset", &MaterialCanvasViewportRequestBus::Events::SelectLightingPreset)
+                ->Event("SelectLightingPresetByName", &MaterialCanvasViewportRequestBus::Events::SelectLightingPresetByName)
+                ->Event("GetLightingPresetNames", &MaterialCanvasViewportRequestBus::Events::GetLightingPresetNames)
+                ->Event("GetLightingPresetLastSavePath", &MaterialCanvasViewportRequestBus::Events::GetLightingPresetLastSavePath)
+                ->Event("AddModelPreset", &MaterialCanvasViewportRequestBus::Events::AddModelPreset)
+                ->Event("SaveModelPreset", &MaterialCanvasViewportRequestBus::Events::SaveModelPreset)
+                ->Event("GetModelPresets", &MaterialCanvasViewportRequestBus::Events::GetModelPresets)
+                ->Event("GetModelPresetByName", &MaterialCanvasViewportRequestBus::Events::GetModelPresetByName)
+                ->Event("GetModelPresetSelection", &MaterialCanvasViewportRequestBus::Events::GetModelPresetSelection)
+                ->Event("SelectModelPreset", &MaterialCanvasViewportRequestBus::Events::SelectModelPreset)
+                ->Event("SelectModelPresetByName", &MaterialCanvasViewportRequestBus::Events::SelectModelPresetByName)
+                ->Event("GetModelPresetNames", &MaterialCanvasViewportRequestBus::Events::GetModelPresetNames)
+                ->Event("GetModelPresetLastSavePath", &MaterialCanvasViewportRequestBus::Events::GetModelPresetLastSavePath)
+                ->Event("SetShadowCatcherEnabled", &MaterialCanvasViewportRequestBus::Events::SetShadowCatcherEnabled)
+                ->Event("GetShadowCatcherEnabled", &MaterialCanvasViewportRequestBus::Events::GetShadowCatcherEnabled)
+                ->Event("SetGridEnabled", &MaterialCanvasViewportRequestBus::Events::SetGridEnabled)
+                ->Event("GetGridEnabled", &MaterialCanvasViewportRequestBus::Events::GetGridEnabled)
+                ->Event("SetAlternateSkyboxEnabled", &MaterialCanvasViewportRequestBus::Events::SetAlternateSkyboxEnabled)
+                ->Event("GetAlternateSkyboxEnabled", &MaterialCanvasViewportRequestBus::Events::GetAlternateSkyboxEnabled)
+                ->Event("SetFieldOfView", &MaterialCanvasViewportRequestBus::Events::SetFieldOfView)
+                ->Event("GetFieldOfView", &MaterialCanvasViewportRequestBus::Events::GetFieldOfView)
+                ;
+
+            behaviorContext->EBus<MaterialCanvasViewportNotificationBus>("MaterialCanvasViewportNotificationBus")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Editor")
+                ->Attribute(AZ::Script::Attributes::Module, "materialcanvas")
+                ->Event("OnLightingPresetAdded", &MaterialCanvasViewportNotificationBus::Events::OnLightingPresetAdded)
+                ->Event("OnLightingPresetSelected", &MaterialCanvasViewportNotificationBus::Events::OnLightingPresetSelected)
+                ->Event("OnLightingPresetChanged", &MaterialCanvasViewportNotificationBus::Events::OnLightingPresetChanged)
+                ->Event("OnModelPresetAdded", &MaterialCanvasViewportNotificationBus::Events::OnModelPresetAdded)
+                ->Event("OnModelPresetSelected", &MaterialCanvasViewportNotificationBus::Events::OnModelPresetSelected)
+                ->Event("OnModelPresetChanged", &MaterialCanvasViewportNotificationBus::Events::OnModelPresetChanged)
+                ->Event("OnShadowCatcherEnabledChanged", &MaterialCanvasViewportNotificationBus::Events::OnShadowCatcherEnabledChanged)
+                ->Event("OnGridEnabledChanged", &MaterialCanvasViewportNotificationBus::Events::OnGridEnabledChanged)
+                ->Event("OnAlternateSkyboxEnabledChanged", &MaterialCanvasViewportNotificationBus::Events::OnAlternateSkyboxEnabledChanged)
+                ->Event("OnFieldOfViewChanged", &MaterialCanvasViewportNotificationBus::Events::OnFieldOfViewChanged)
+                ;
+        }
+    }
+
+    void MaterialCanvasViewportComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required)
+    {
+        required.push_back(AZ_CRC_CE("RPISystem"));
+        required.push_back(AZ_CRC_CE("AssetDatabaseService"));
+        required.push_back(AZ_CRC_CE("PerformanceMonitorService"));
+    }
+
+    void MaterialCanvasViewportComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
+    {
+        provided.push_back(AZ_CRC_CE("MaterialCanvasViewportService"));
+    }
+
+    void MaterialCanvasViewportComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
+    {
+        incompatible.push_back(AZ_CRC_CE("MaterialCanvasViewportService"));
+    }
+
+    void MaterialCanvasViewportComponent::Init()
+    {
+    }
+
+    void MaterialCanvasViewportComponent::Activate()
+    {
+        m_viewportSettings =
+            AZ::UserSettings::CreateFind<MaterialCanvasViewportSettings>(AZ::Crc32("MaterialCanvasViewportSettings"), AZ::UserSettings::CT_GLOBAL);
+
+        MaterialCanvasViewportRequestBus::Handler::BusConnect();
+        AzFramework::AssetCatalogEventBus::Handler::BusConnect();
+    }
+
+    void MaterialCanvasViewportComponent::Deactivate()
+    {
+        AzFramework::AssetCatalogEventBus::Handler::BusDisconnect();
+        MaterialCanvasViewportRequestBus::Handler::BusDisconnect();
+        ClearContent();
+    }
+
+    void MaterialCanvasViewportComponent::ClearContent()
+    {
+        AZ::Data::AssetBus::MultiHandler::BusDisconnect();
+
+        m_lightingPresetAssets.clear();
+        m_lightingPresetVector.clear();
+        m_lightingPresetLastSavePathMap.clear();
+        m_lightingPresetSelection.reset();
+
+        m_modelPresetAssets.clear();
+        m_modelPresetVector.clear();
+        m_modelPresetLastSavePathMap.clear();
+        m_modelPresetSelection.reset();
+    }
+
+    void MaterialCanvasViewportComponent::ReloadContent()
+    {
+        AZ_TracePrintf("Material Canvas", "Started loading viewport configurations.\n");
+
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnBeginReloadContent);
+
+        ClearContent();
+
+        // Enumerate and load all the relevant preset files in the project.
+        // (The files are stored in a temporary list instead of processed in the callback because deep operations inside
+        // AssetCatalogRequestBus::EnumerateAssets can lead to deadlocked)
+        AZ::Data::AssetCatalogRequests::AssetEnumerationCB enumerateCB = [this]([[maybe_unused]] const AZ::Data::AssetId id, const AZ::Data::AssetInfo& info)
+        {
+            if (AZ::StringFunc::EndsWith(info.m_relativePath.c_str(), ".lightingpreset.azasset"))
+            {
+                m_lightingPresetAssets[info.m_assetId] = { info.m_assetId, info.m_assetType };
+                AZ::Data::AssetBus::MultiHandler::BusConnect(info.m_assetId);
+                return;
+            }
+
+            if (AZ::StringFunc::EndsWith(info.m_relativePath.c_str(), ".modelpreset.azasset"))
+            {
+                m_modelPresetAssets[info.m_assetId] = { info.m_assetId, info.m_assetType };
+                AZ::Data::AssetBus::MultiHandler::BusConnect(info.m_assetId);
+                return;
+            }
+        };
+
+        AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets, nullptr, enumerateCB, nullptr);
+
+        for (auto& assetPair : m_lightingPresetAssets)
+        {
+            assetPair.second.QueueLoad();
+        }
+
+        for (auto& assetPair : m_modelPresetAssets)
+        {
+            assetPair.second.QueueLoad();
+        }
+    }
+
+    AZ::Render::LightingPresetPtr MaterialCanvasViewportComponent::AddLightingPreset(const AZ::Render::LightingPreset& preset)
+    {
+        m_lightingPresetVector.push_back(AZStd::make_shared<AZ::Render::LightingPreset>(preset));
+        auto presetPtr = m_lightingPresetVector.back();
+
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnLightingPresetAdded, presetPtr);
+        return presetPtr;
+    }
+
+    AZ::Render::LightingPresetPtr MaterialCanvasViewportComponent::GetLightingPresetByName(const AZStd::string& name) const
+    {
+        const auto presetItr = AZStd::find_if(m_lightingPresetVector.begin(), m_lightingPresetVector.end(), [&name](const auto& preset) {
+            return preset && preset->m_displayName == name; });
+        return presetItr != m_lightingPresetVector.end() ? *presetItr : nullptr;
+    }
+
+    AZ::Render::LightingPresetPtrVector MaterialCanvasViewportComponent::GetLightingPresets() const
+    {
+        return m_lightingPresetVector;
+    }
+
+    bool MaterialCanvasViewportComponent::SaveLightingPreset(AZ::Render::LightingPresetPtr preset, const AZStd::string& path) const
+    {
+        if (preset && AZ::JsonSerializationUtils::SaveObjectToFile<AZ::Render::LightingPreset>(preset.get(), path).IsSuccess())
+        {
+            m_lightingPresetLastSavePathMap[preset] = path;
+            return true;
+        }
+
+        return false;
+    }
+
+    AZ::Render::LightingPresetPtr MaterialCanvasViewportComponent::GetLightingPresetSelection() const
+    {
+        return m_lightingPresetSelection;
+    }
+
+    void MaterialCanvasViewportComponent::SelectLightingPreset(AZ::Render::LightingPresetPtr preset)
+    {
+        if (preset)
+        {
+            m_lightingPresetSelection = preset;
+            m_viewportSettings->m_selectedLightingPresetName = preset->m_displayName;
+            MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnLightingPresetSelected, m_lightingPresetSelection);
+        }
+    }
+
+    void MaterialCanvasViewportComponent::SelectLightingPresetByName(const AZStd::string& name)
+    {
+        SelectLightingPreset(GetLightingPresetByName(name));
+    }
+
+    MaterialCanvasViewportPresetNameSet MaterialCanvasViewportComponent::GetLightingPresetNames() const
+    {
+        MaterialCanvasViewportPresetNameSet names;
+        for (const auto& preset : m_lightingPresetVector)
+        {
+            if (preset)
+            {
+                names.insert(preset->m_displayName);
+            }
+        }
+        return names;
+    }
+
+    AZStd::string MaterialCanvasViewportComponent::GetLightingPresetLastSavePath(AZ::Render::LightingPresetPtr preset) const
+    {
+        auto pathItr = m_lightingPresetLastSavePathMap.find(preset);
+        return pathItr != m_lightingPresetLastSavePathMap.end() ? pathItr->second : AZStd::string();
+    }
+
+    AZ::Render::ModelPresetPtr MaterialCanvasViewportComponent::AddModelPreset(const AZ::Render::ModelPreset& preset)
+    {
+        m_modelPresetVector.push_back(AZStd::make_shared<AZ::Render::ModelPreset>(preset));
+        auto presetPtr = m_modelPresetVector.back();
+
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnModelPresetAdded, presetPtr);
+        return presetPtr;
+    }
+
+    AZ::Render::ModelPresetPtr MaterialCanvasViewportComponent::GetModelPresetByName(const AZStd::string& name) const
+    {
+        const auto presetItr = AZStd::find_if(m_modelPresetVector.begin(), m_modelPresetVector.end(), [&name](const auto& preset) {
+            return preset && preset->m_displayName == name; });
+        return presetItr != m_modelPresetVector.end() ? *presetItr : nullptr;
+    }
+
+    AZ::Render::ModelPresetPtrVector MaterialCanvasViewportComponent::GetModelPresets() const
+    {
+        return m_modelPresetVector;
+    }
+
+    bool MaterialCanvasViewportComponent::SaveModelPreset(AZ::Render::ModelPresetPtr preset, const AZStd::string& path) const
+    {
+        if (preset && AZ::JsonSerializationUtils::SaveObjectToFile<AZ::Render::ModelPreset>(preset.get(), path).IsSuccess())
+        {
+            m_modelPresetLastSavePathMap[preset] = path;
+            return true;
+        }
+
+        return false;
+    }
+
+    AZ::Render::ModelPresetPtr MaterialCanvasViewportComponent::GetModelPresetSelection() const
+    {
+        return m_modelPresetSelection;
+    }
+
+    void MaterialCanvasViewportComponent::SelectModelPreset(AZ::Render::ModelPresetPtr preset)
+    {
+        if (preset)
+        {
+            m_modelPresetSelection = preset;
+            m_viewportSettings->m_selectedModelPresetName = preset->m_displayName;
+            MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnModelPresetSelected, m_modelPresetSelection);
+        }
+    }
+
+    void MaterialCanvasViewportComponent::SelectModelPresetByName(const AZStd::string& name)
+    {
+        SelectModelPreset(GetModelPresetByName(name));
+    }
+
+    MaterialCanvasViewportPresetNameSet MaterialCanvasViewportComponent::GetModelPresetNames() const
+    {
+        MaterialCanvasViewportPresetNameSet names;
+        for (const auto& preset : m_modelPresetVector)
+        {
+            if (preset)
+            {
+                names.insert(preset->m_displayName);
+            }
+        }
+        return names;
+    }
+
+    AZStd::string MaterialCanvasViewportComponent::GetModelPresetLastSavePath(AZ::Render::ModelPresetPtr preset) const
+    {
+        auto pathItr = m_modelPresetLastSavePathMap.find(preset);
+        return pathItr != m_modelPresetLastSavePathMap.end() ? pathItr->second : AZStd::string();
+    }
+
+    void MaterialCanvasViewportComponent::SetShadowCatcherEnabled(bool enable)
+    {
+        m_viewportSettings->m_enableShadowCatcher = enable;
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnShadowCatcherEnabledChanged, enable);
+    }
+
+    bool MaterialCanvasViewportComponent::GetShadowCatcherEnabled() const
+    {
+        return m_viewportSettings->m_enableShadowCatcher;
+    }
+
+    void MaterialCanvasViewportComponent::SetGridEnabled(bool enable)
+    {
+        m_viewportSettings->m_enableGrid = enable;
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnGridEnabledChanged, enable);
+    }
+
+
+    bool MaterialCanvasViewportComponent::GetGridEnabled() const
+    {
+        return m_viewportSettings->m_enableGrid;
+    }
+
+    void MaterialCanvasViewportComponent::SetAlternateSkyboxEnabled(bool enable)
+    {
+        m_viewportSettings->m_enableAlternateSkybox = enable;
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnAlternateSkyboxEnabledChanged, enable);
+    }
+
+
+    bool MaterialCanvasViewportComponent::GetAlternateSkyboxEnabled() const
+    {
+        return m_viewportSettings->m_enableAlternateSkybox;
+    }
+
+    void MaterialCanvasViewportComponent::SetFieldOfView(float fieldOfView)
+    {
+        m_viewportSettings->m_fieldOfView = fieldOfView;
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnFieldOfViewChanged, fieldOfView);
+    }
+
+
+    float MaterialCanvasViewportComponent::GetFieldOfView() const
+    {
+        return m_viewportSettings->m_fieldOfView;
+    }
+
+    void MaterialCanvasViewportComponent::SetDisplayMapperOperationType(AZ::Render::DisplayMapperOperationType operationType)
+    {
+        m_viewportSettings->m_displayMapperOperationType = operationType;
+        MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnDisplayMapperOperationTypeChanged, operationType);
+    }
+
+    AZ::Render::DisplayMapperOperationType MaterialCanvasViewportComponent::GetDisplayMapperOperationType() const
+    {
+        return m_viewportSettings->m_displayMapperOperationType;
+    }
+
+    inline void MaterialCanvasViewportComponent::OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset)
+    {
+        if (AZ::Data::Asset<AZ::RPI::AnyAsset> anyAsset = asset)
+        {
+            if (const auto lightingPreset = anyAsset->GetDataAs<AZ::Render::LightingPreset>())
+            {
+                auto presetPtr = AddLightingPreset(*lightingPreset);
+                const auto& presetPath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(anyAsset.GetId());
+                m_lightingPresetAssets[anyAsset.GetId()] = anyAsset;
+                m_lightingPresetLastSavePathMap[presetPtr] = presetPath;
+                AZ_TracePrintf("Material Canvas", "Loaded Preset: %s\n", presetPath.c_str());
+            }
+
+            if (const auto modelPreset = anyAsset->GetDataAs<AZ::Render::ModelPreset>())
+            {
+                auto presetPtr = AddModelPreset(*modelPreset);
+                const auto& presetPath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(anyAsset.GetId());
+                m_modelPresetAssets[anyAsset.GetId()] = anyAsset;
+                m_modelPresetLastSavePathMap[presetPtr] = presetPath;
+                AZ_TracePrintf("Material Canvas", "Loaded Preset: %s\n", presetPath.c_str());
+            }
+        }
+
+        AZ::Data::AssetBus::MultiHandler::BusDisconnect(asset.GetId());
+        if (!AZ::Data::AssetBus::MultiHandler::BusIsConnected())
+        {
+            SelectLightingPresetByName(m_viewportSettings->m_selectedLightingPresetName);
+            SelectModelPresetByName(m_viewportSettings->m_selectedModelPresetName);
+            MaterialCanvasViewportNotificationBus::Broadcast(&MaterialCanvasViewportNotificationBus::Events::OnEndReloadContent);
+            AZ_TracePrintf("Material Canvas", "Finished loading viewport configurations.\n");
+        }
+    }
+
+    void MaterialCanvasViewportComponent::OnCatalogLoaded([[maybe_unused]] const char* catalogFile)
+    {
+        AZ::TickBus::QueueFunction([this]() {
+            ReloadContent();
+        });
+    }
+
+    void MaterialCanvasViewportComponent::OnCatalogAssetChanged(const AZ::Data::AssetId& assetId)
+    {
+        auto ReloadLightingAndModelPresets = [this, &assetId](AZ::Data::AssetCatalogRequests* assetCatalogRequests)
+        {
+            AZ::Data::AssetInfo assetInfo = assetCatalogRequests->GetAssetInfoById(assetId);
+            if (AZ::StringFunc::EndsWith(assetInfo.m_relativePath.c_str(), ".lightingpreset.azasset"))
+            {
+                m_lightingPresetAssets[assetInfo.m_assetId] = { assetInfo.m_assetId, assetInfo.m_assetType };
+                m_lightingPresetAssets[assetInfo.m_assetId].QueueLoad();
+                AZ::Data::AssetBus::MultiHandler::BusConnect(assetInfo.m_assetId);
+                return;
+            }
+
+            if (AzFramework::StringFunc::EndsWith(assetInfo.m_relativePath.c_str(), ".modelpreset.azasset"))
+            {
+                m_modelPresetAssets[assetInfo.m_assetId] = { assetInfo.m_assetId, assetInfo.m_assetType };
+                m_modelPresetAssets[assetInfo.m_assetId].QueueLoad();
+                AZ::Data::AssetBus::MultiHandler::BusConnect(assetInfo.m_assetId);
+                return;
+            }
+        };
+        AZ::Data::AssetCatalogRequestBus::Broadcast(AZStd::move(ReloadLightingAndModelPresets));
+    }
+
+    void MaterialCanvasViewportComponent::OnCatalogAssetAdded(const AZ::Data::AssetId& assetId)
+    {
+        OnCatalogAssetChanged(assetId);
+    }
+
+    void MaterialCanvasViewportComponent::OnCatalogAssetRemoved(const AZ::Data::AssetId& assetId, const AZ::Data::AssetInfo& assetInfo)
+    {
+        if (AZ::StringFunc::EndsWith(assetInfo.m_relativePath.c_str(), ".lightingpreset.azasset"))
+        {
+            AZ::Data::AssetBus::MultiHandler::BusDisconnect(assetInfo.m_assetId);
+            m_lightingPresetAssets.erase(assetId);
+            return;
+        }
+
+        if (AZ::StringFunc::EndsWith(assetInfo.m_relativePath.c_str(), ".modelpreset.azasset"))
+        {
+            AZ::Data::AssetBus::MultiHandler::BusDisconnect(assetInfo.m_assetId);
+            m_modelPresetAssets.erase(assetId);
+            return;
+        }
+    }
+}

+ 116 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportComponent.h

@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <ACES/Aces.h>
+#include <Atom/Feature/Utils/LightingPreset.h>
+#include <Atom/Feature/Utils/ModelPreset.h>
+#include <Atom/RPI.Reflect/System/AnyAsset.h>
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Component/Component.h>
+#include <AzFramework/Asset/AssetCatalogBus.h>
+#include <Viewport/MaterialCanvasViewportRequestBus.h>
+#include <Viewport/MaterialCanvasViewportSettings.h>
+
+namespace MaterialCanvas
+{
+    //! MaterialCanvasViewportComponent registers reflected datatypes and manages different configurations for lighting and models displayed in the viewport
+    class MaterialCanvasViewportComponent
+        : public AZ::Component
+        , private MaterialCanvasViewportRequestBus::Handler
+        , private AZ::Data::AssetBus::MultiHandler
+        , private AzFramework::AssetCatalogEventBus::Handler
+    {
+    public:
+        AZ_COMPONENT(MaterialCanvasViewportComponent, "{A92305C3-32AB-4D50-BE4D-430FCF436C4E}");
+
+        MaterialCanvasViewportComponent();
+        ~MaterialCanvasViewportComponent() = default;
+        MaterialCanvasViewportComponent(const MaterialCanvasViewportComponent&) = delete;
+        MaterialCanvasViewportComponent& operator =(const MaterialCanvasViewportComponent&) = delete;
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
+        static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
+        static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
+
+    private:
+        ////////////////////////////////////////////////////////////////////////
+         // AZ::Component interface implementation
+        void Init() override;
+        void Activate() override;
+        void Deactivate() override;
+        ////////////////////////////////////////////////////////////////////////
+
+        void ClearContent();
+
+        ////////////////////////////////////////////////////////////////////////
+        // MaterialCanvasViewportRequestBus::Handler overrides ...
+        void ReloadContent() override;
+
+        AZ::Render::LightingPresetPtr AddLightingPreset(const AZ::Render::LightingPreset& preset) override;
+        AZ::Render::LightingPresetPtrVector GetLightingPresets() const override;
+        bool SaveLightingPreset(AZ::Render::LightingPresetPtr preset, const AZStd::string& path) const override;
+        AZ::Render::LightingPresetPtr GetLightingPresetByName(const AZStd::string& name) const override;
+        AZ::Render::LightingPresetPtr GetLightingPresetSelection() const override;
+        void SelectLightingPreset(AZ::Render::LightingPresetPtr preset) override;
+        void SelectLightingPresetByName(const AZStd::string& name) override;
+        MaterialCanvasViewportPresetNameSet GetLightingPresetNames() const override;
+        AZStd::string GetLightingPresetLastSavePath(AZ::Render::LightingPresetPtr preset) const override;
+
+        AZ::Render::ModelPresetPtr AddModelPreset(const AZ::Render::ModelPreset& preset) override;
+        AZ::Render::ModelPresetPtrVector GetModelPresets() const override;
+        bool SaveModelPreset(AZ::Render::ModelPresetPtr preset, const AZStd::string& path) const override;
+        AZ::Render::ModelPresetPtr GetModelPresetByName(const AZStd::string& name) const override;
+        AZ::Render::ModelPresetPtr GetModelPresetSelection() const override;
+        void SelectModelPreset(AZ::Render::ModelPresetPtr preset) override;
+        void SelectModelPresetByName(const AZStd::string& name) override;
+        MaterialCanvasViewportPresetNameSet GetModelPresetNames() const override;
+        AZStd::string GetModelPresetLastSavePath(AZ::Render::ModelPresetPtr preset) const override;
+
+        void SetShadowCatcherEnabled(bool enable) override;
+        bool GetShadowCatcherEnabled() const override;
+        void SetGridEnabled(bool enable) override;
+        bool GetGridEnabled() const override;
+        void SetAlternateSkyboxEnabled(bool enable) override;
+        bool GetAlternateSkyboxEnabled() const override;
+        void SetFieldOfView(float fieldOfView) override;
+        float GetFieldOfView() const override;
+        void SetDisplayMapperOperationType(AZ::Render::DisplayMapperOperationType operationType) override;
+        AZ::Render::DisplayMapperOperationType GetDisplayMapperOperationType() const override;
+        ////////////////////////////////////////////////////////////////////////
+
+        ////////////////////////////////////////////////////////////////////////
+        // AZ::Data::AssetBus::MultiHandler overrides ...
+        void OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
+        ////////////////////////////////////////////////////////////////////////
+
+        ////////////////////////////////////////////////////////////////////////
+        // AzFramework::AssetCatalogEventBus::Handler overrides ...
+        void OnCatalogLoaded(const char* catalogFile) override;
+        void OnCatalogAssetChanged(const AZ::Data::AssetId& assetId) override;
+        void OnCatalogAssetAdded(const AZ::Data::AssetId& assetId) override;
+        void OnCatalogAssetRemoved(const AZ::Data::AssetId& assetId, const AZ::Data::AssetInfo& assetInfo) override;
+        ////////////////////////////////////////////////////////////////////////
+
+        AZStd::unordered_map<AZ::Data::AssetId, AZ::Data::Asset<AZ::RPI::AnyAsset>> m_lightingPresetAssets;
+        AZ::Render::LightingPresetPtrVector m_lightingPresetVector;
+        AZ::Render::LightingPresetPtr m_lightingPresetSelection;
+
+        AZStd::unordered_map<AZ::Data::AssetId, AZ::Data::Asset<AZ::RPI::AnyAsset>> m_modelPresetAssets;
+        AZ::Render::ModelPresetPtrVector m_modelPresetVector;
+        AZ::Render::ModelPresetPtr m_modelPresetSelection;
+
+        mutable AZStd::map<AZ::Render::LightingPresetPtr, AZStd::string> m_lightingPresetLastSavePathMap;
+        mutable AZStd::map<AZ::Render::ModelPresetPtr, AZStd::string> m_modelPresetLastSavePathMap;
+
+        AZStd::intrusive_ptr<MaterialCanvasViewportSettings> m_viewportSettings;
+    };
+}

+ 30 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportModule.cpp

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Viewport/MaterialCanvasViewportComponent.h>
+#include <Viewport/MaterialCanvasViewportModule.h>
+
+namespace MaterialCanvas
+{
+    MaterialCanvasViewportModule::MaterialCanvasViewportModule()
+    {
+        // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
+        m_descriptors.insert(
+            m_descriptors.end(),
+            {
+                MaterialCanvasViewportComponent::CreateDescriptor(),
+            });
+    }
+
+    AZ::ComponentTypeList MaterialCanvasViewportModule::GetRequiredSystemComponents() const
+    {
+        return AZ::ComponentTypeList{
+            azrtti_typeid<MaterialCanvasViewportComponent>(),
+        };
+    }
+} // namespace MaterialCanvas

+ 28 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportModule.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Module/Module.h>
+
+namespace MaterialCanvas
+{
+    //! Entry point for viewport library. This module is responsible for registering and reflecting dependencies
+    class MaterialCanvasViewportModule
+        : public AZ::Module
+    {
+    public:
+        AZ_RTTI(MaterialCanvasViewportModule, "{D4A53226-D31A-4FBD-A599-F17F740EC2BA}", AZ::Module);
+        AZ_CLASS_ALLOCATOR(MaterialCanvasViewportModule, AZ::SystemAllocator, 0);
+
+        MaterialCanvasViewportModule();
+
+        //! Add required SystemComponents to the SystemEntity.
+        AZ::ComponentTypeList GetRequiredSystemComponents() const override;
+    };
+}

+ 71 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportNotificationBus.h

@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <ACES/Aces.h>
+#include <AzCore/EBus/EBus.h>
+#include <Atom/Feature/Utils/LightingPreset.h>
+#include <Atom/Feature/Utils/ModelPreset.h>
+
+namespace MaterialCanvas
+{
+    class MaterialCanvasViewportNotifications
+        : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
+
+        //! Signal that all configs are about to be reloaded
+        virtual void OnBeginReloadContent() {}
+
+        //! Signal that all configs were reloaded
+        virtual void OnEndReloadContent() {}
+
+        //! Signal that a preset was added
+        //! @param preset being added
+        virtual void OnLightingPresetAdded([[maybe_unused]] AZ::Render::LightingPresetPtr preset) {}
+
+        //! Signal that a preset was selected
+        //! @param preset being selected
+        virtual void OnLightingPresetSelected([[maybe_unused]] AZ::Render::LightingPresetPtr preset) {}
+
+        //! Signal that a preset was changed
+        //! @param preset being changed
+        virtual void OnLightingPresetChanged([[maybe_unused]] AZ::Render::LightingPresetPtr preset) {}
+
+        //! Signal that a preset was added
+        //! @param preset being added
+        virtual void OnModelPresetAdded([[maybe_unused]] AZ::Render::ModelPresetPtr preset) {}
+
+        //! Signal that a preset was selected
+        //! @param preset being selected
+        virtual void OnModelPresetSelected([[maybe_unused]] AZ::Render::ModelPresetPtr preset) {}
+
+        //! Signal that a preset was changed
+        //! @param preset being changed
+        virtual void OnModelPresetChanged([[maybe_unused]] AZ::Render::ModelPresetPtr preset) {}
+
+        //! Notify when enabled state for shadow catcher changes
+        virtual void OnShadowCatcherEnabledChanged([[maybe_unused]] bool enable) {}
+
+        //! Notify when enabled state for grid changes
+        virtual void OnGridEnabledChanged([[maybe_unused]] bool enable) {}
+
+        //! Notify when enabled state for alternate skybox changes
+        virtual void OnAlternateSkyboxEnabledChanged([[maybe_unused]] bool enable) {}
+
+        //! Notify when field of view changes
+        virtual void OnFieldOfViewChanged([[maybe_unused]] float fieldOfView) {}
+
+        //! Notify when tone mapping changes
+        virtual void OnDisplayMapperOperationTypeChanged([[maybe_unused]] AZ::Render::DisplayMapperOperationType operationType) {}
+    };
+
+    using MaterialCanvasViewportNotificationBus = AZ::EBus<MaterialCanvasViewportNotifications>;
+} // namespace MaterialCanvas

+ 138 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportRequestBus.h

@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <ACES/Aces.h>
+#include <AzCore/EBus/EBus.h>
+#include <AzCore/std/containers/set.h>
+#include <AzCore/std/string/string.h>
+#include <Atom/Feature/Utils/LightingPreset.h>
+#include <Atom/Feature/Utils/ModelPreset.h>
+#include <QImage>
+
+namespace MaterialCanvas
+{
+    using MaterialCanvasViewportPresetNameSet = AZStd::set<AZStd::string>;
+
+    class MaterialCanvasViewportRequests
+        : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
+        static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
+
+        //! Reload all presets
+        virtual void ReloadContent() = 0;
+
+        //! Add lighting preset
+        //! @param preset lighting preset to add for selection
+        //! @returns pointer to new, managed preset
+        virtual AZ::Render::LightingPresetPtr AddLightingPreset(const AZ::Render::LightingPreset& preset) = 0;
+
+        //! Get lighting presets
+        //! @returns all presets
+        virtual AZ::Render::LightingPresetPtrVector GetLightingPresets() const = 0;
+
+        //! Save lighting preset
+        //! @returns true if preset was saved, otherwise false
+        virtual bool SaveLightingPreset(AZ::Render::LightingPresetPtr preset, const AZStd::string& path) const = 0;
+
+        //! Get lighting preset by name
+        //! @param name preset name to search for
+        //! @returns the requested preset if found, otherwise nullptr
+        virtual AZ::Render::LightingPresetPtr GetLightingPresetByName(const AZStd::string& name) const = 0;
+
+        //! Get selected lighting preset
+        //! @returns selected preset if found, otherwise nullptr
+        virtual AZ::Render::LightingPresetPtr GetLightingPresetSelection() const = 0;
+
+        //! Select lighting preset
+        //! @param preset to select
+        virtual void SelectLightingPreset(AZ::Render::LightingPresetPtr preset) = 0;
+
+        //! Select lighting preset by name
+        //! @param name preset name to select
+        virtual void SelectLightingPresetByName(const AZStd::string& name) = 0;
+
+        //! Get set of lighting preset names
+        virtual MaterialCanvasViewportPresetNameSet GetLightingPresetNames() const = 0;
+
+        //! Get model preset last save path
+        //! @param preset to lookup last save path
+        virtual AZStd::string GetLightingPresetLastSavePath(AZ::Render::LightingPresetPtr preset) const = 0;
+
+        //! Add model preset
+        //! @param preset model preset to add for selection
+        //! @returns pointer to new, managed preset
+        virtual AZ::Render::ModelPresetPtr AddModelPreset(const AZ::Render::ModelPreset& preset) = 0;
+
+        //! Get model presets
+        //! @returns all presets
+        virtual AZ::Render::ModelPresetPtrVector GetModelPresets() const = 0;
+
+        //! Save Model preset
+        //! @returns true if preset was saved, otherwise false
+        virtual bool SaveModelPreset(AZ::Render::ModelPresetPtr preset, const AZStd::string& path) const = 0;
+
+        //! Get model preset by name
+        //! @param name preset name to search for
+        //! @returns the requested preset if found, otherwise nullptr
+        virtual AZ::Render::ModelPresetPtr GetModelPresetByName(const AZStd::string& name) const = 0;
+
+        //! Get selected model preset
+        //! @returns selected preset if found, otherwise nullptr
+        virtual AZ::Render::ModelPresetPtr GetModelPresetSelection() const = 0;
+
+        //! Select lighting preset
+        //! @param name preset to select
+        virtual void SelectModelPreset(AZ::Render::ModelPresetPtr preset) = 0;
+
+        //! Select model preset by name
+        //! @param name preset name to select
+        virtual void SelectModelPresetByName(const AZStd::string& name) = 0;
+
+        //! Get set of model preset names
+        virtual MaterialCanvasViewportPresetNameSet GetModelPresetNames() const = 0;
+
+        //! Get model preset last save path
+        //! @param preset to lookup last save path
+        virtual AZStd::string GetModelPresetLastSavePath(AZ::Render::ModelPresetPtr preset) const = 0;
+
+        //! Set enabled state for shadow catcher
+        virtual void SetShadowCatcherEnabled(bool enable) = 0;
+
+        //! Get enabled state for shadow catcher
+        virtual bool GetShadowCatcherEnabled() const = 0;
+
+        //! Set enabled state for grid
+        virtual void SetGridEnabled(bool enable) = 0;
+
+        //! Get enabled state for grid
+        virtual bool GetGridEnabled() const = 0;
+
+        //! Set enabled state for alternate skybox
+        virtual void SetAlternateSkyboxEnabled(bool enable) = 0;
+
+        //! Get enabled state for alternate skybox
+        virtual bool GetAlternateSkyboxEnabled() const = 0;
+
+        //! Set field of view
+        virtual void SetFieldOfView(float fieldOfView) = 0;
+
+        //! Get field of view
+        virtual float GetFieldOfView() const = 0;
+
+        //! Set tone mapping type
+        virtual void SetDisplayMapperOperationType(AZ::Render::DisplayMapperOperationType operationType) = 0;
+
+        //! Get tone mapping type
+        virtual AZ::Render::DisplayMapperOperationType GetDisplayMapperOperationType() const = 0;
+    };
+
+    using MaterialCanvasViewportRequestBus = AZ::EBus<MaterialCanvasViewportRequests>;
+} // namespace MaterialCanvas

+ 68 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportSettings.cpp

@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/RTTI/BehaviorContext.h>
+#include <AzCore/Serialization/EditContext.h>
+#include <Viewport/MaterialCanvasViewportSettings.h>
+
+namespace MaterialCanvas
+{
+    void MaterialCanvasViewportSettings::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MaterialCanvasViewportSettings, AZ::UserSettings>()
+                ->Version(1)
+                ->Field("enableGrid", &MaterialCanvasViewportSettings::m_enableGrid)
+                ->Field("enableShadowCatcher", &MaterialCanvasViewportSettings::m_enableShadowCatcher)
+                ->Field("enableAlternateSkybox", &MaterialCanvasViewportSettings::m_enableAlternateSkybox)
+                ->Field("fieldOfView", &MaterialCanvasViewportSettings::m_fieldOfView)
+                ->Field("displayMapperOperationType", &MaterialCanvasViewportSettings::m_displayMapperOperationType)
+                ->Field("selectedModelPresetName", &MaterialCanvasViewportSettings::m_selectedModelPresetName)
+                ->Field("selectedLightingPresetName", &MaterialCanvasViewportSettings::m_selectedLightingPresetName)
+            ;
+
+            if (auto editContext = serializeContext->GetEditContext())
+            {
+                editContext->Class<MaterialCanvasViewportSettings>(
+                    "MaterialCanvasViewportSettings", "")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &MaterialCanvasViewportSettings::m_enableGrid, "Enable Grid", "")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &MaterialCanvasViewportSettings::m_enableShadowCatcher, "Enable Shadow Catcher", "")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &MaterialCanvasViewportSettings::m_enableAlternateSkybox, "Enable Alternate Skybox", "")
+                    ->DataElement(AZ::Edit::UIHandlers::Slider, &MaterialCanvasViewportSettings::m_fieldOfView, "Field Of View", "")
+                        ->Attribute(AZ::Edit::Attributes::Min, 60.0f)
+                        ->Attribute(AZ::Edit::Attributes::Max, 120.0f)
+                    ->DataElement(AZ::Edit::UIHandlers::ComboBox, &MaterialCanvasViewportSettings::m_displayMapperOperationType, "Display Mapper Type", "")
+                    ->EnumAttribute(AZ::Render::DisplayMapperOperationType::Aces, "Aces")
+                    ->EnumAttribute(AZ::Render::DisplayMapperOperationType::AcesLut, "AcesLut")
+                    ->EnumAttribute(AZ::Render::DisplayMapperOperationType::Passthrough, "Passthrough")
+                    ->EnumAttribute(AZ::Render::DisplayMapperOperationType::GammaSRGB, "GammaSRGB")
+                    ->EnumAttribute(AZ::Render::DisplayMapperOperationType::Reinhard, "Reinhard")
+                    ;
+            }
+        }
+
+        if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->Class<MaterialCanvasViewportSettings>("MaterialCanvasViewportSettings")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Editor")
+                ->Attribute(AZ::Script::Attributes::Module, "materialcanvas")
+                ->Constructor()
+                ->Constructor<const MaterialCanvasViewportSettings&>()
+                ->Property("enableGrid", BehaviorValueProperty(&MaterialCanvasViewportSettings::m_enableGrid))
+                ->Property("enableShadowCatcher", BehaviorValueProperty(&MaterialCanvasViewportSettings::m_enableShadowCatcher))
+                ->Property("enableAlternateSkybox", BehaviorValueProperty(&MaterialCanvasViewportSettings::m_enableAlternateSkybox))
+                ->Property("fieldOfView", BehaviorValueProperty(&MaterialCanvasViewportSettings::m_fieldOfView))
+                ->Property("displayMapperOperationType", BehaviorValueProperty(&MaterialCanvasViewportSettings::m_displayMapperOperationType))
+                ;
+        }
+    }
+} // namespace MaterialCanvas

+ 37 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportSettings.h

@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <ACES/Aces.h>
+#include <AzCore/Memory/Memory.h>
+#include <AzCore/RTTI/RTTI.h>
+#include <AzCore/RTTI/ReflectContext.h>
+#include <AzCore/UserSettings/UserSettings.h>
+#endif
+
+namespace MaterialCanvas
+{
+    struct MaterialCanvasViewportSettings
+        : public AZ::UserSettings
+    {
+        AZ_RTTI(MaterialCanvasViewportSettings, "{16150503-A314-4765-82A3-172670C9EA90}", AZ::UserSettings);
+        AZ_CLASS_ALLOCATOR(MaterialCanvasViewportSettings, AZ::SystemAllocator, 0);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        bool m_enableGrid = true;
+        bool m_enableShadowCatcher = true;
+        bool m_enableAlternateSkybox = false;
+        float m_fieldOfView = 90.0f;
+        AZ::Render::DisplayMapperOperationType m_displayMapperOperationType = AZ::Render::DisplayMapperOperationType::Aces;
+        AZStd::string m_selectedModelPresetName = "Shader Ball";
+        AZStd::string m_selectedLightingPresetName = "Neutral Urban";
+    };
+} // namespace MaterialCanvas

+ 475 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportWidget.cpp

@@ -0,0 +1,475 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#undef RC_INVOKED
+
+#include <Atom/Component/DebugCamera/CameraComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+#include <Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h>
+#include <Atom/Feature/ImageBasedLights/ImageBasedLightFeatureProcessorInterface.h>
+#include <Atom/Feature/PostProcess/PostProcessFeatureProcessorInterface.h>
+#include <Atom/Feature/PostProcessing/PostProcessingConstants.h>
+#include <Atom/Feature/Utils/LightingPreset.h>
+#include <Atom/Feature/Utils/ModelPreset.h>
+#include <Atom/RHI/Device.h>
+#include <Atom/RHI/RHISystemInterface.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+#include <Atom/RPI.Public/Material/Material.h>
+#include <Atom/RPI.Public/Pass/Specific/SwapChainPass.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/RenderPipeline.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Public/ViewportContext.h>
+#include <Atom/RPI.Public/ViewportContextBus.h>
+#include <Atom/RPI.Public/WindowContext.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <AtomCore/Instance/InstanceDatabase.h>
+#include <AtomLyIntegration/CommonFeatures/Grid/GridComponentConfig.h>
+#include <AtomLyIntegration/CommonFeatures/Grid/GridComponentConstants.h>
+#include <AtomLyIntegration/CommonFeatures/ImageBasedLights/ImageBasedLightComponentBus.h>
+#include <AtomLyIntegration/CommonFeatures/ImageBasedLights/ImageBasedLightComponentConstants.h>
+#include <AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h>
+#include <AtomLyIntegration/CommonFeatures/Material/MaterialComponentConstants.h>
+#include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h>
+#include <AtomLyIntegration/CommonFeatures/Mesh/MeshComponentConstants.h>
+#include <AtomLyIntegration/CommonFeatures/PostProcess/ExposureControl/ExposureControlBus.h>
+#include <AtomLyIntegration/CommonFeatures/PostProcess/ExposureControl/ExposureControlComponentConstants.h>
+#include <AtomLyIntegration/CommonFeatures/PostProcess/PostFxLayerComponentConstants.h>
+#include <AzCore/Component/Component.h>
+#include <AzCore/Component/Entity.h>
+#include <AzFramework/Components/NonUniformScaleComponent.h>
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Entity/GameEntityContextBus.h>
+#include <AzFramework/Viewport/ViewportControllerList.h>
+#include <Document/MaterialCanvasDocumentRequestBus.h>
+#include <Viewport/MaterialCanvasViewportRequestBus.h>
+#include <Viewport/MaterialCanvasViewportSettings.h>
+#include <Viewport/MaterialCanvasViewportWidget.h>
+#include <Viewport/ui_MaterialCanvasViewportWidget.h>
+
+AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
+#include <QWindow>
+AZ_POP_DISABLE_WARNING
+
+namespace MaterialCanvas
+{
+    static constexpr float DepthNear = 0.01f;
+
+    MaterialCanvasViewportWidget::MaterialCanvasViewportWidget(QWidget* parent)
+        : AtomToolsFramework::RenderViewportWidget(parent)
+        , m_ui(new Ui::MaterialCanvasViewportWidget)
+        , m_viewportController(AZStd::make_shared<MaterialCanvasViewportInputController>())
+    {
+        m_ui->setupUi(this);
+
+        // The viewport context created by AtomToolsFramework::RenderViewportWidget has no name.
+        // Systems like frame capturing and post FX expect there to be a context with DefaultViewportContextName
+        auto viewportContextManager = AZ::Interface<AZ::RPI::ViewportContextRequestsInterface>::Get();
+        const AZ::Name defaultContextName = viewportContextManager->GetDefaultViewportContextName();
+        viewportContextManager->RenameViewportContext(GetViewportContext(), defaultContextName);
+
+        // Create and register a scene with all available feature processors
+        AZ::RPI::SceneDescriptor sceneDesc;
+        sceneDesc.m_nameId = AZ::Name("MaterialCanvasViewport");
+        m_scene = AZ::RPI::Scene::CreateScene(sceneDesc);
+        m_scene->EnableAllFeatureProcessors();
+
+        // Bind m_defaultScene to the GameEntityContext's AzFramework::Scene
+        auto sceneSystem = AzFramework::SceneSystemInterface::Get();
+        AZ_Assert(sceneSystem, "MaterialCanvasViewportWidget was unable to get the scene system during construction.");
+        AZStd::shared_ptr<AzFramework::Scene> mainScene = sceneSystem->GetScene(AzFramework::Scene::MainSceneName);
+
+        // This should never happen unless scene creation has changed.
+        AZ_Assert(mainScene, "Main scenes missing during system component initialization");
+        mainScene->SetSubsystem(m_scene);
+
+        // Create a render pipeline from the specified asset for the window context and add the pipeline to the scene
+        AZ::Data::Asset<AZ::RPI::AnyAsset> pipelineAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath<AZ::RPI::AnyAsset>(
+            m_defaultPipelineAssetPath.c_str(), AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_renderPipeline = AZ::RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineAsset, *GetViewportContext()->GetWindowContext().get());
+        pipelineAsset.Release();
+        m_scene->AddRenderPipeline(m_renderPipeline);
+
+        // As part of our initialization we need to create the BRDF texture generation pipeline
+        AZ::RPI::RenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.m_mainViewTagName = "MainCamera";
+        pipelineDesc.m_name = "BRDFTexturePipeline";
+        pipelineDesc.m_rootPassTemplate = "BRDFTexturePipeline";
+        pipelineDesc.m_executeOnce = true;
+
+        AZ::RPI::RenderPipelinePtr brdfTexturePipeline = AZ::RPI::RenderPipeline::CreateRenderPipeline(pipelineDesc);
+        m_scene->AddRenderPipeline(brdfTexturePipeline);
+
+        // Currently the scene has to be activated after render pipeline was added so some feature processors (i.e. imgui) can be
+        // initialized properly with pipeline's pass information.
+        m_scene->Activate();
+
+        AZ::RPI::RPISystemInterface::Get()->RegisterScene(m_scene);
+
+        AzFramework::EntityContextId entityContextId;
+        AzFramework::GameEntityContextRequestBus::BroadcastResult(
+            entityContextId, &AzFramework::GameEntityContextRequestBus::Events::GetGameEntityContextId);
+
+        // Configure camera
+        AzFramework::EntityContextRequestBus::EventResult(
+            m_cameraEntity, entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, "Cameraentity");
+        AZ_Assert(m_cameraEntity != nullptr, "Failed to create camera entity.");
+
+        // Add debug camera and controller components
+        AZ::Debug::CameraComponentConfig cameraConfig(GetViewportContext()->GetWindowContext());
+        cameraConfig.m_fovY = AZ::Constants::HalfPi;
+        cameraConfig.m_depthNear = DepthNear;
+        m_cameraComponent = m_cameraEntity->CreateComponent(azrtti_typeid<AZ::Debug::CameraComponent>());
+        m_cameraComponent->SetConfiguration(cameraConfig);
+        m_cameraEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_cameraEntity->Activate();
+
+        // Connect camera to pipeline's default view after camera entity activated
+        m_renderPipeline->SetDefaultViewFromEntity(m_cameraEntity->GetId());
+
+        // Configure tone mapper
+        AzFramework::EntityContextRequestBus::EventResult(
+            m_postProcessEntity, entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, "postProcessEntity");
+        AZ_Assert(m_postProcessEntity != nullptr, "Failed to create post process entity.");
+
+        m_postProcessEntity->CreateComponent(AZ::Render::PostFxLayerComponentTypeId);
+        m_postProcessEntity->CreateComponent(AZ::Render::ExposureControlComponentTypeId);
+        m_postProcessEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_postProcessEntity->Activate();
+
+        // Init directional light processor
+        m_directionalLightFeatureProcessor = m_scene->GetFeatureProcessor<AZ::Render::DirectionalLightFeatureProcessorInterface>();
+
+        // Init display mapper processor
+        m_displayMapperFeatureProcessor = m_scene->GetFeatureProcessor<AZ::Render::DisplayMapperFeatureProcessorInterface>();
+
+        // Init Skybox
+        m_skyboxFeatureProcessor = m_scene->GetFeatureProcessor<AZ::Render::SkyBoxFeatureProcessorInterface>();
+        m_skyboxFeatureProcessor->Enable(true);
+        m_skyboxFeatureProcessor->SetSkyboxMode(AZ::Render::SkyBoxMode::Cubemap);
+
+        // Create IBL
+        AzFramework::EntityContextRequestBus::EventResult(
+            m_iblEntity, entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, "IblEntity");
+        AZ_Assert(m_iblEntity != nullptr, "Failed to create ibl entity.");
+
+        m_iblEntity->CreateComponent(AZ::Render::ImageBasedLightComponentTypeId);
+        m_iblEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_iblEntity->Activate();
+
+        // Create model
+        AzFramework::EntityContextRequestBus::EventResult(
+            m_modelEntity, entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, "ViewportModel");
+        AZ_Assert(m_modelEntity != nullptr, "Failed to create model entity.");
+
+        m_modelEntity->CreateComponent(AZ::Render::MeshComponentTypeId);
+        m_modelEntity->CreateComponent(AZ::Render::MaterialComponentTypeId);
+        m_modelEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_modelEntity->Activate();
+
+        // Create shadow catcher
+        AzFramework::EntityContextRequestBus::EventResult(
+            m_shadowCatcherEntity, entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, "ViewportShadowCatcher");
+        AZ_Assert(m_shadowCatcherEntity != nullptr, "Failed to create shadow catcher entity.");
+        m_shadowCatcherEntity->CreateComponent(AZ::Render::MeshComponentTypeId);
+        m_shadowCatcherEntity->CreateComponent(AZ::Render::MaterialComponentTypeId);
+        m_shadowCatcherEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_shadowCatcherEntity->CreateComponent(azrtti_typeid<AzFramework::NonUniformScaleComponent>());
+        m_shadowCatcherEntity->Activate();
+
+        AZ::NonUniformScaleRequestBus::Event(
+            m_shadowCatcherEntity->GetId(), &AZ::NonUniformScaleRequests::SetScale, AZ::Vector3{ 100, 100, 1.0 });
+
+        AZ::Data::AssetId shadowCatcherModelAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(
+            "materialcanvas/viewportmodels/plane_1x1.azmodel", AZ::RPI::AssetUtils::TraceLevel::Error);
+        AZ::Render::MeshComponentRequestBus::Event(
+            m_shadowCatcherEntity->GetId(), &AZ::Render::MeshComponentRequestBus::Events::SetModelAssetId, shadowCatcherModelAssetId);
+
+        auto shadowCatcherMaterialAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath<AZ::RPI::MaterialAsset>(
+            "materials/special/shadowcatcher.azmaterial", AZ::RPI::AssetUtils::TraceLevel::Error);
+        if (shadowCatcherMaterialAsset)
+        {
+            m_shadowCatcherOpacityPropertyIndex =
+                shadowCatcherMaterialAsset->GetMaterialTypeAsset()->GetMaterialPropertiesLayout()->FindPropertyIndex(
+                    AZ::Name{ "settings.opacity" });
+            AZ_Error("MaterialCanvasViewportWidget", m_shadowCatcherOpacityPropertyIndex.IsValid(), "Could not find opacity property");
+
+            m_shadowCatcherMaterial = AZ::RPI::Material::Create(shadowCatcherMaterialAsset);
+            AZ_Error("MaterialCanvasViewportWidget", m_shadowCatcherMaterial != nullptr, "Could not create shadow catcher material.");
+
+            AZ::Render::MaterialAssignmentMap shadowCatcherMaterials;
+            auto& shadowCatcherMaterialAssignment = shadowCatcherMaterials[AZ::Render::DefaultMaterialAssignmentId];
+            shadowCatcherMaterialAssignment.m_materialInstance = m_shadowCatcherMaterial;
+            shadowCatcherMaterialAssignment.m_materialInstancePreCreated = true;
+
+            AZ::Render::MaterialComponentRequestBus::Event(
+                m_shadowCatcherEntity->GetId(), &AZ::Render::MaterialComponentRequestBus::Events::SetMaterialOverrides,
+                shadowCatcherMaterials);
+        }
+
+        // Create grid
+        AzFramework::EntityContextRequestBus::EventResult(
+            m_gridEntity, entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, "ViewportGrid");
+        AZ_Assert(m_gridEntity != nullptr, "Failed to create grid entity.");
+
+        AZ::Render::GridComponentConfig gridConfig;
+        gridConfig.m_gridSize = 4.0f;
+        gridConfig.m_axisColor = AZ::Color(0.1f, 0.1f, 0.1f, 1.0f);
+        gridConfig.m_primaryColor = AZ::Color(0.1f, 0.1f, 0.1f, 1.0f);
+        gridConfig.m_secondaryColor = AZ::Color(0.1f, 0.1f, 0.1f, 1.0f);
+        auto gridComponent = m_gridEntity->CreateComponent(AZ::Render::GridComponentTypeId);
+        gridComponent->SetConfiguration(gridConfig);
+
+        m_gridEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_gridEntity->Activate();
+
+        OnDocumentOpened(AZ::Uuid::CreateNull());
+
+        // Attempt to apply the default lighting preset
+        AZ::Render::LightingPresetPtr lightingPreset;
+        MaterialCanvasViewportRequestBus::BroadcastResult(lightingPreset, &MaterialCanvasViewportRequestBus::Events::GetLightingPresetSelection);
+        OnLightingPresetSelected(lightingPreset);
+
+        // Attempt to apply the default model preset
+        AZ::Render::ModelPresetPtr modelPreset;
+        MaterialCanvasViewportRequestBus::BroadcastResult(modelPreset, &MaterialCanvasViewportRequestBus::Events::GetModelPresetSelection);
+        OnModelPresetSelected(modelPreset);
+
+        m_viewportController->Init(m_cameraEntity->GetId(), m_modelEntity->GetId(), m_iblEntity->GetId());
+
+        // Apply user settinngs restored since last run
+        AZStd::intrusive_ptr<MaterialCanvasViewportSettings> viewportSettings =
+            AZ::UserSettings::CreateFind<MaterialCanvasViewportSettings>(AZ::Crc32("MaterialCanvasViewportSettings"), AZ::UserSettings::CT_GLOBAL);
+
+        OnGridEnabledChanged(viewportSettings->m_enableGrid);
+        OnShadowCatcherEnabledChanged(viewportSettings->m_enableShadowCatcher);
+        OnAlternateSkyboxEnabledChanged(viewportSettings->m_enableAlternateSkybox);
+        OnFieldOfViewChanged(viewportSettings->m_fieldOfView);
+        OnDisplayMapperOperationTypeChanged(viewportSettings->m_displayMapperOperationType);
+
+        AtomToolsFramework::AtomToolsDocumentNotificationBus::Handler::BusConnect();
+        MaterialCanvasViewportNotificationBus::Handler::BusConnect();
+        AZ::TickBus::Handler::BusConnect();
+        AZ::TransformNotificationBus::MultiHandler::BusConnect(m_cameraEntity->GetId());
+
+        GetControllerList()->Add(m_viewportController);
+    }
+
+    MaterialCanvasViewportWidget::~MaterialCanvasViewportWidget()
+    {
+        AZ::TransformNotificationBus::MultiHandler::BusDisconnect();
+        AZ::TickBus::Handler::BusDisconnect();
+        AtomToolsFramework::AtomToolsDocumentNotificationBus::Handler::BusDisconnect();
+        MaterialCanvasViewportNotificationBus::Handler::BusDisconnect();
+        AZ::Data::AssetBus::Handler::BusDisconnect();
+
+        AzFramework::EntityContextId entityContextId;
+        AzFramework::GameEntityContextRequestBus::BroadcastResult(
+            entityContextId, &AzFramework::GameEntityContextRequestBus::Events::GetGameEntityContextId);
+
+        AzFramework::EntityContextRequestBus::Event(
+            entityContextId, &AzFramework::EntityContextRequestBus::Events::DestroyEntity, m_iblEntity);
+        m_iblEntity = nullptr;
+
+        AzFramework::EntityContextRequestBus::Event(
+            entityContextId, &AzFramework::EntityContextRequestBus::Events::DestroyEntity, m_modelEntity);
+        m_modelEntity = nullptr;
+
+        AzFramework::EntityContextRequestBus::Event(
+            entityContextId, &AzFramework::EntityContextRequestBus::Events::DestroyEntity, m_shadowCatcherEntity);
+        m_shadowCatcherEntity = nullptr;
+
+        AzFramework::EntityContextRequestBus::Event(
+            entityContextId, &AzFramework::EntityContextRequestBus::Events::DestroyEntity, m_gridEntity);
+        m_gridEntity = nullptr;
+
+        AzFramework::EntityContextRequestBus::Event(
+            entityContextId, &AzFramework::EntityContextRequestBus::Events::DestroyEntity, m_cameraEntity);
+        m_cameraEntity = nullptr;
+
+        AzFramework::EntityContextRequestBus::Event(
+            entityContextId, &AzFramework::EntityContextRequestBus::Events::DestroyEntity, m_postProcessEntity);
+        m_postProcessEntity = nullptr;
+
+        for (DirectionalLightHandle& handle : m_lightHandles)
+        {
+            m_directionalLightFeatureProcessor->ReleaseLight(handle);
+        }
+        m_lightHandles.clear();
+
+        auto sceneSystem = AzFramework::SceneSystemInterface::Get();
+        AZ_Assert(sceneSystem, "MaterialCanvasViewportWidget was unable to get the scene system during destruction.");
+        AZStd::shared_ptr<AzFramework::Scene> mainScene = sceneSystem->GetScene(AzFramework::Scene::MainSceneName);
+        // This should never happen unless scene creation has changed.
+        AZ_Assert(mainScene, "Main scenes missing during system component destruction");
+        mainScene->UnsetSubsystem(m_scene);
+
+        m_swapChainPass = nullptr;
+        AZ::RPI::RPISystemInterface::Get()->UnregisterScene(m_scene);
+        m_scene = nullptr;
+    }
+
+    void MaterialCanvasViewportWidget::OnDocumentOpened(const AZ::Uuid& /*documentId*/)
+    {
+    }
+
+    void MaterialCanvasViewportWidget::OnLightingPresetSelected(AZ::Render::LightingPresetPtr preset)
+    {
+        if (!preset)
+        {
+            return;
+        }
+
+        AZ::Render::ImageBasedLightFeatureProcessorInterface* iblFeatureProcessor =
+            m_scene->GetFeatureProcessor<AZ::Render::ImageBasedLightFeatureProcessorInterface>();
+        AZ::Render::PostProcessFeatureProcessorInterface* postProcessFeatureProcessor =
+            m_scene->GetFeatureProcessor<AZ::Render::PostProcessFeatureProcessorInterface>();
+
+        AZ::Render::ExposureControlSettingsInterface* exposureControlSettingInterface =
+            postProcessFeatureProcessor->GetOrCreateSettingsInterface(m_postProcessEntity->GetId())
+                ->GetOrCreateExposureControlSettingsInterface();
+
+        Camera::Configuration cameraConfig;
+        Camera::CameraRequestBus::EventResult(
+            cameraConfig, m_cameraEntity->GetId(), &Camera::CameraRequestBus::Events::GetCameraConfiguration);
+
+        bool enableAlternateSkybox = false;
+        MaterialCanvasViewportRequestBus::BroadcastResult(enableAlternateSkybox, &MaterialCanvasViewportRequestBus::Events::GetAlternateSkyboxEnabled);
+
+        preset->ApplyLightingPreset(
+            iblFeatureProcessor, m_skyboxFeatureProcessor, exposureControlSettingInterface, m_directionalLightFeatureProcessor,
+            cameraConfig, m_lightHandles, m_shadowCatcherMaterial, m_shadowCatcherOpacityPropertyIndex, enableAlternateSkybox);
+    }
+
+    void MaterialCanvasViewportWidget::OnLightingPresetChanged(AZ::Render::LightingPresetPtr preset)
+    {
+        AZ::Render::LightingPresetPtr selectedPreset;
+        MaterialCanvasViewportRequestBus::BroadcastResult(selectedPreset, &MaterialCanvasViewportRequestBus::Events::GetLightingPresetSelection);
+        if (selectedPreset == preset)
+        {
+            OnLightingPresetSelected(preset);
+        }
+    }
+
+    void MaterialCanvasViewportWidget::OnModelPresetSelected(AZ::Render::ModelPresetPtr preset)
+    {
+        if (!preset)
+        {
+            return;
+        }
+
+        if (!preset->m_modelAsset.GetId().IsValid())
+        {
+            AZ_Warning(
+                "MaterialCanvasViewportWidget", false, "Attempting to set invalid model for preset: '%s'\n.", preset->m_displayName.c_str());
+            return;
+        }
+
+        if (preset->m_modelAsset.GetId() == m_modelAssetId)
+        {
+            return;
+        }
+
+        AZ::Render::MeshComponentRequestBus::Event(
+            m_modelEntity->GetId(), &AZ::Render::MeshComponentRequestBus::Events::SetModelAsset, preset->m_modelAsset);
+
+        m_modelAssetId = preset->m_modelAsset.GetId();
+
+        AZ::Data::AssetBus::Handler::BusDisconnect();
+        AZ::Data::AssetBus::Handler::BusConnect(m_modelAssetId);
+    }
+
+    void MaterialCanvasViewportWidget::OnModelPresetChanged(AZ::Render::ModelPresetPtr preset)
+    {
+        AZ::Render::ModelPresetPtr selectedPreset;
+        MaterialCanvasViewportRequestBus::BroadcastResult(selectedPreset, &MaterialCanvasViewportRequestBus::Events::GetModelPresetSelection);
+        if (selectedPreset == preset)
+        {
+            OnModelPresetSelected(preset);
+        }
+    }
+
+    void MaterialCanvasViewportWidget::OnShadowCatcherEnabledChanged(bool enable)
+    {
+        AZ::Render::MeshComponentRequestBus::Event(
+            m_shadowCatcherEntity->GetId(), &AZ::Render::MeshComponentRequestBus::Events::SetVisibility, enable);
+    }
+
+    void MaterialCanvasViewportWidget::OnGridEnabledChanged(bool enable)
+    {
+        if (m_gridEntity)
+        {
+            if (enable && m_gridEntity->GetState() == AZ::Entity::State::Init)
+            {
+                m_gridEntity->Activate();
+            }
+            else if (!enable && m_gridEntity->GetState() == AZ::Entity::State::Active)
+            {
+                m_gridEntity->Deactivate();
+            }
+        }
+    }
+
+    void MaterialCanvasViewportWidget::OnAlternateSkyboxEnabledChanged(bool enable)
+    {
+        AZ_UNUSED(enable);
+        AZ::Render::LightingPresetPtr selectedPreset;
+        MaterialCanvasViewportRequestBus::BroadcastResult(selectedPreset, &MaterialCanvasViewportRequestBus::Events::GetLightingPresetSelection);
+        OnLightingPresetSelected(selectedPreset);
+    }
+
+    void MaterialCanvasViewportWidget::OnFieldOfViewChanged(float fieldOfView)
+    {
+        MaterialCanvasViewportInputControllerRequestBus::Broadcast(
+            &MaterialCanvasViewportInputControllerRequestBus::Handler::SetFieldOfView, fieldOfView);
+    }
+
+    void MaterialCanvasViewportWidget::OnDisplayMapperOperationTypeChanged(AZ::Render::DisplayMapperOperationType operationType)
+    {
+        AZ::Render::DisplayMapperConfigurationDescriptor desc;
+        desc.m_operationType = operationType;
+        m_displayMapperFeatureProcessor->RegisterDisplayMapperConfiguration(desc);
+    }
+
+    void MaterialCanvasViewportWidget::OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset)
+    {
+        if (m_modelAssetId == asset.GetId())
+        {
+            MaterialCanvasViewportInputControllerRequestBus::Broadcast(&MaterialCanvasViewportInputControllerRequestBus::Handler::Reset);
+            AZ::Data::AssetBus::Handler::BusDisconnect(asset.GetId());
+        }
+    }
+
+    void MaterialCanvasViewportWidget::OnTick(float deltaTime, AZ::ScriptTimePoint time)
+    {
+        AtomToolsFramework::RenderViewportWidget::OnTick(deltaTime, time);
+
+        m_renderPipeline->AddToRenderTickOnce();
+
+        if (m_shadowCatcherMaterial)
+        {
+            // Compile the m_shadowCatcherMaterial in OnTick because changes can only be compiled once per frame.
+            // This is ignored when a compile isn't needed.
+            m_shadowCatcherMaterial->Compile();
+        }
+    }
+
+    void MaterialCanvasViewportWidget::OnTransformChanged(const AZ::Transform&, const AZ::Transform&)
+    {
+        const AZ::EntityId* currentBusId = AZ::TransformNotificationBus::GetCurrentBusId();
+        if (m_cameraEntity && currentBusId && *currentBusId == m_cameraEntity->GetId() && m_directionalLightFeatureProcessor)
+        {
+            auto transform = AZ::Transform::CreateIdentity();
+            AZ::TransformBus::EventResult(transform, m_cameraEntity->GetId(), &AZ::TransformBus::Events::GetWorldTM);
+            for (const DirectionalLightHandle& id : m_lightHandles)
+            {
+                m_directionalLightFeatureProcessor->SetCameraTransform(id, transform);
+            }
+        }
+    }
+} // namespace MaterialCanvas

+ 119 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportWidget.h

@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+#include <Atom/RPI.Public/Base.h>
+#include <AtomCore/Instance/Instance.h>
+#include <AtomToolsFramework/Document/AtomToolsDocumentNotificationBus.h>
+#include <AtomToolsFramework/Viewport/RenderViewportWidget.h>
+#include <AzCore/Component/TransformBus.h>
+#include <AzFramework/Windowing/WindowBus.h>
+#include <Viewport/InputController/MaterialCanvasViewportInputController.h>
+#include <Viewport/MaterialCanvasViewportNotificationBus.h>
+
+AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
+#include <QWidget>
+AZ_POP_DISABLE_WARNING
+#endif
+
+namespace AZ
+{
+    namespace Render
+    {
+        class DisplayMapperFeatureProcessorInterface;
+    }
+
+    class Entity;
+    class Component;
+
+    namespace RPI
+    {
+        class SwapChainPass;
+        class WindowContext;
+    } // namespace RPI
+} // namespace AZ
+
+namespace Ui
+{
+    class MaterialCanvasViewportWidget;
+}
+
+namespace MaterialCanvas
+{
+    class MaterialCanvasViewportWidget
+        : public AtomToolsFramework::RenderViewportWidget
+        , public AZ::Data::AssetBus::Handler
+        , public AtomToolsFramework::AtomToolsDocumentNotificationBus::Handler
+        , public MaterialCanvasViewportNotificationBus::Handler
+        , public AZ::TransformNotificationBus::MultiHandler
+    {
+    public:
+        MaterialCanvasViewportWidget(QWidget* parent = nullptr);
+        ~MaterialCanvasViewportWidget();
+
+    private:
+        // AtomToolsFramework::AtomToolsDocumentNotificationBus::Handler interface overrides...
+        void OnDocumentOpened(const AZ::Uuid& documentId) override;
+
+        // MaterialCanvasViewportNotificationBus::Handler interface overrides...
+        void OnLightingPresetSelected(AZ::Render::LightingPresetPtr preset) override;
+        void OnLightingPresetChanged(AZ::Render::LightingPresetPtr preset) override;
+        void OnModelPresetSelected(AZ::Render::ModelPresetPtr preset) override;
+        void OnModelPresetChanged(AZ::Render::ModelPresetPtr preset) override;
+        void OnShadowCatcherEnabledChanged(bool enable) override;
+        void OnGridEnabledChanged(bool enable) override;
+        void OnAlternateSkyboxEnabledChanged(bool enable) override;
+        void OnFieldOfViewChanged(float fieldOfView) override;
+        void OnDisplayMapperOperationTypeChanged(AZ::Render::DisplayMapperOperationType operationType) override;
+
+        // AZ::Data::AssetBus::Handler interface overrides...
+        void OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
+
+        // AZ::TickBus::Handler interface overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+        // AZ::TransformNotificationBus::MultiHandler overrides...
+        void OnTransformChanged(const AZ::Transform&, const AZ::Transform&) override;
+
+        using DirectionalLightHandle = AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle;
+
+        AZ::Data::Instance<AZ::RPI::SwapChainPass> m_swapChainPass;
+        AZStd::string m_defaultPipelineAssetPath = "passes/MainRenderPipeline.azasset";
+        AZ::RPI::RenderPipelinePtr m_renderPipeline;
+        AZ::RPI::ScenePtr m_scene;
+        AZ::Render::DirectionalLightFeatureProcessorInterface* m_directionalLightFeatureProcessor = {};
+        AZ::Render::DisplayMapperFeatureProcessorInterface* m_displayMapperFeatureProcessor = {};
+
+        AZ::Entity* m_cameraEntity = {};
+        AZ::Component* m_cameraComponent = {};
+
+        AZ::Entity* m_postProcessEntity = {};
+
+        AZ::Entity* m_modelEntity = {};
+        AZ::Data::AssetId m_modelAssetId;
+
+        AZ::Entity* m_gridEntity = {};
+
+        AZ::Entity* m_shadowCatcherEntity = {};
+        AZ::Data::Instance<AZ::RPI::Material> m_shadowCatcherMaterial;
+        AZ::RPI::MaterialPropertyIndex m_shadowCatcherOpacityPropertyIndex;
+
+        AZStd::vector<DirectionalLightHandle> m_lightHandles;
+
+        AZ::Entity* m_iblEntity = {};
+        AZ::Render::SkyBoxFeatureProcessorInterface* m_skyboxFeatureProcessor = {};
+
+        AZStd::shared_ptr<MaterialCanvasViewportInputController> m_viewportController;
+
+        QScopedPointer<Ui::MaterialCanvasViewportWidget> m_ui;
+    };
+} // namespace MaterialCanvas

+ 50 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Viewport/MaterialCanvasViewportWidget.ui

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MaterialCanvasViewportWidget</class>
+ <widget class="QWidget" name="MaterialCanvasViewportWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>869</width>
+    <height>574</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Material Viewport</string>
+  </property>
+  <property name="autoFillBackground">
+   <bool>false</bool>
+  </property>
+  <property name="styleSheet">
+   <string notr="true"/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QWidget" name="widget" native="true">
+     <layout class="QHBoxLayout" name="horizontalLayout"/>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/add.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4fa1646134c32e57a5ea1e7c5e32325be158c089317397638e90b61127cbb856
+size 223

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/addnew.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf016edacd48e20dd5841e7dda10ba8811195307d55544a6278819253d75fa1e
+size 224

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/down.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8e49d78ef9dbbc1c41a1e4cd7f8b5d32bcd3fb65782d5fe5cca1dd0145e0dd2e
+size 347

+ 9 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/grid.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background: #444444;">
+    <!-- Generator: Sketch 64 (93537) - https://sketch.com -->
+    <title>Icons / Toolbar / Ground</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Icons-/-Toolbar-/-Ground" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <path d="M2,19 L5,6 L19,6 L22,19 L2,19 Z M5,14.667 L4.462,17 L8.051,17 L8.255,15 L5,15 L5,14.667 Z M19,14.667 L19,15 L15.699,15 L15.9,17 L19.538,17 L19,14.667 Z M13.699,15 L10.255,15 L10.051,17 L13.9,17 L13.699,15 Z M5.269,13.5 L8.407,13.5 L8.611,11.5 L5.731,11.5 L5.269,13.5 Z M18.269,11.5 L15.348,11.5 L15.549,13.5 L18.731,13.5 L18.269,11.5 Z M13.348,11.5 L10.611,11.5 L10.407,13.5 L13.549,13.5 L13.348,11.5 Z M6.077,10 L8.763,10 L8.967,8 L6.538,8 L6.077,10 Z M17.462,8 L10.967,8 L10.763,10 L13.198,10 L13,8.02631406 L15,8.02631406 L15.198,10 L17.923,10 L17.462,8 Z" id="Combined-Shape" fill="#FFFFFF"></path>
+    </g>
+</svg>

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/material.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Icons / Editor / Material Variant</title>
+    <g id="Icons-/-Editor-/-Material-Variant" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <rect id="Icon-Background" x="0" y="0" width="24" height="24"></rect>
+        <rect id="Icon-Background" x="0" y="0" width="24" height="24"></rect>
+        <rect id="Rectangle" fill="#EEEEEE" fill-rule="nonzero" x="16" y="15.9999948" width="6" height="6"></rect>
+        <path d="M13.2003238,2.05728467 L13.3206336,2.08541409 C17.9843907,2.69389766 21.611637,6.4842271 21.9707625,11.2247926 C21.9900321,11.3980031 22,11.5711198 22,11.7433304 L21.997,11.815 L22,12 C22,12.6856005 21.9319119,13.3545263 21.802092,14.0004211 L14,14 L14.0004211,21.802092 C13.4430737,21.9141145 12.8685772,21.980171 12.2810157,21.9961774 L12,22 C6.45454545,22 2,17.5454545 2,12 C2,6.45454545 6.45454545,2 12,2 C12.1802332,2 12.359314,2.00470542 12.537127,2.01400079 C12.719362,1.98546889 12.9435559,2.00149401 13.2003238,2.05728467 Z M11.999,15.636 L9.54545455,15.6363636 C10.0721003,18.3573668 11.1072317,20.0613988 11.9144212,20.1756828 L11.999,20.181 L11.999,15.636 Z M7.72727273,15.6363636 L4.63636364,15.6363636 C5.54545455,17.4545455 7.09090909,18.8181818 8.90909091,19.6363636 C8.36363636,18.5454545 8,17.0909091 7.72727273,15.6363636 Z M11.999,10.181 L9.36363636,10.1818182 C9.27272727,10.7272727 9.27272727,11.3636364 9.27272727,12 C9.27272727,12.5090909 9.27272727,13.0181818 9.31927273,13.4807273 L9.36363636,13.8181818 L11.999,13.818 L11.999,10.181 Z M7.45454545,10.1818182 L4,10.1818182 C3.90909091,10.7272727 3.81818182,11.3636364 3.81818182,12 C3.81818182,12.5090909 3.87636364,13.0181818 3.94618182,13.4807273 L4,13.8181818 L7.54545455,13.8181818 C7.47272727,13.3090909 7.45818182,12.8581818 7.45527273,12.3723636 L7.45454545,12 L7.45454545,10.1818182 Z M11.9990157,3.81818265 C11.2091528,3.8195194 10.1655566,5.42953176 9.60376892,8.07613162 L9.54545455,8.36363636 L11.999,8.363 L11.9990157,3.81818265 Z M9,4.36363636 C7.1969697,5.13636364 5.71829405,6.3956229 4.79372428,8.0648304 L4.63636364,8.36363636 L7.72727273,8.36363636 C8,6.90909091 8.45454545,5.45454545 9,4.36363636 Z" id="Combined-Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
+    </g>
+</svg>

Разлика између датотеке није приказан због своје велике величине
+ 15 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/materialcanvas.svg


Разлика између датотеке није приказан због своје велике величине
+ 7 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/materialtype.svg


Разлика између датотеке није приказан због своје велике величине
+ 6 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/mesh.svg


+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/refresh.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:36c55e5e6cfdb9b0e387a6b0a7de6888cde6372d5d00aa2f918c9873a888fd83
+size 302

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/remove.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab737fbb49b456dd9d0cb92f6f5e6c70d6bfd67871a6a1fed2907dc602bd70c2
+size 358

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4de0f4a102e5c4990d5d23e22b5cdd16532192f3a125758b608cd8c731278898
+size 355

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save_active.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d0b6f7c69112d124eac1123ffa840268c16148fef9ded3a67f84d3ad8d73eb96
+size 212

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save_disabled.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0b795fb88a6f301d7ecc9ce75141bdcf9e6dca25043730661aa4d1f09b055d6f
+size 222

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/save_normal.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:88fbce753cdd9381a814e3b2ec5d16df7cc26f2a37ad30b7010be71ae9183510
+size 214

+ 9 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/shadow.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background: #444444;">
+    <!-- Generator: Sketch 64 (93537) - https://sketch.com -->
+    <title>Icons / Toolbar / Shadow</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Icons-/-Toolbar-/-Shadow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <path d="M6.99979618,18.810136 L9.18779618,20.999136 L7,21 L6.99979618,18.810136 Z M17,3 L17,7.312 L20.9997962,11.310136 L20.9997962,13.189136 L17,9.19 L17,11.311 L20.9997962,15.310136 L20.9997962,17.689136 L17,13.69 L17,15.812 L20.9997962,19.811136 L21,21 L19.8097962,20.999136 L15.81,17 L13.689,17 L17.6887962,20.999136 L15.3097962,20.999136 L11.31,17 L9.188,17 L13.1877962,20.999136 L11.3097962,20.999136 L7.311,17 L3,17 L3,3 L17,3 Z M21,7 L20.9997962,9.18913601 L18.8087962,6.99913601 L21,7 Z" id="Combined-Shape" fill="#FFFFFF"></path>
+    </g>
+</svg>

+ 6 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/skybox.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M22.3438 17.1096C22.3438 17.4789 22.1402 17.8181 21.8144 17.9919L12.4933 22.9632C12.1997 23.1197 11.8476 23.1201 11.5538 22.9641L2.18687 17.9915C1.86006 17.818 1.65576 17.4783 1.65576 17.1083V7.01127C1.65576 6.64874 1.85197 6.31461 2.16855 6.13798L11.0673 1.17335C11.6754 0.834121 12.4161 0.835535 13.0228 1.17709L21.8343 6.13729C22.149 6.31445 22.3438 6.64755 22.3438 7.0087V17.1096ZM18.5409 15.0325C18.4611 15.3007 18.3148 15.558 18.0922 15.7637L20.0423 16.8389L12.0216 21.1724L3.93436 16.8389L6.15868 15.6225C5.97079 15.422 5.80027 15.1916 5.66768 14.9666L3.58362 15.9796V7.38525L11.5543 3.0379V5.15478C11.9078 5.05561 12.2944 5.10892 12.6106 5.27134V3.0379L20.4596 7.38525V15.9796L18.5409 15.0325Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2539 15.7233C14.6484 16.1002 15.2132 16.3362 15.8405 16.3362C17.0332 16.3362 18 15.4831 18 14.4307C18 13.7082 17.5443 13.0796 16.8726 12.7566C16.9535 12.5453 16.9976 12.3178 16.9976 12.0806C16.9976 10.9698 16.0308 10.0693 14.8381 10.0693C13.6455 10.0693 12.6786 10.9698 12.6786 12.0806L12.6786 12.0885C12.2188 12.0885 11.846 12.4357 11.846 12.864C11.846 12.9208 11.8525 12.9761 11.865 13.0295C11.8017 13.0186 11.7369 13.0074 11.6705 12.9957C10.4155 12.7742 9.33382 13.6413 9.33382 14.6001C9.33382 15.5589 9.94577 15.9127 11.1967 16.3785C12.0329 16.5537 13.4996 16.6779 14.2539 15.7233Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.56534 13.9147C7.87582 13.2747 7.44183 12.3443 7.44183 11.3086C7.44183 9.3783 8.94955 7.81348 10.8094 7.81348C11.9738 7.81348 13.0002 8.42682 13.605 9.3592C13.1565 9.55085 12.9243 9.83046 12.758 10.0721C12.472 10.4875 12.2905 10.9511 12.2905 11.286C12.1112 11.1809 11.7089 11.3109 11.5191 11.5007C11.3294 11.6904 11.2139 12.0368 11.2764 12.32L11.269 12.3179C11.0257 12.2484 10.4232 12.076 9.71747 12.4856C9.18575 12.7942 8.75166 13.2238 8.56534 13.9147Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7198 5.5C11.1341 5.50049 11.4694 5.83668 11.4689 6.25089L11.4676 7.4149C11.225 7.36682 10.9741 7.34161 10.7173 7.34161C10.4607 7.34161 10.21 7.36679 9.96757 7.41481L9.96895 6.24911C9.96944 5.8349 10.3056 5.49951 10.7198 5.5ZM7.62108 8.9143C7.91801 8.50756 8.29357 8.16156 8.7255 7.89854L7.96371 7.11207C7.67552 6.81455 7.2007 6.80698 6.90318 7.09517C6.60566 7.38336 6.59809 7.85818 6.88628 8.1557L7.62108 8.9143ZM6.91223 11.5693C6.89769 11.4341 6.89023 11.2968 6.89023 11.1578C6.89023 10.7796 6.9454 10.4142 7.04816 10.0693H5.81698C5.40277 10.0693 5.06698 10.4051 5.06698 10.8193C5.06698 11.2335 5.40277 11.5693 5.81698 11.5693H6.91223ZM8.06611 13.9099C7.75724 13.614 7.49807 13.2669 7.30234 12.8822L6.30037 13.6199C5.96681 13.8655 5.89548 14.3349 6.14106 14.6685C6.38663 15.0021 6.85612 15.0734 7.18968 14.8278L8.1963 14.0867L8.06611 13.9099ZM12.8339 7.97783C13.2546 8.25681 13.6168 8.61658 13.8981 9.03493L14.6921 8.24344C14.9854 7.95101 14.9862 7.47613 14.6938 7.18278C14.4013 6.88943 13.9265 6.88869 13.6331 7.18112L12.8339 7.97783Z" fill="white"/>
+</svg>

+ 10 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/texture.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Icons / Editor / Image</title>
+    <g id="Icons-/-Editor-/-Image" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <rect id="Icon-Background" x="0" y="0" width="24" height="24"></rect>
+        <g id="picture" transform="translate(3.000000, 3.000000)" fill="#FFFFFF" fill-rule="nonzero">
+            <path d="M14.550959,11.6 L12,8.6 L11,9.6 L8,6.6 L3.46109347,11.6 L3.46109347,14.5401062 L14.550959,14.5401062 L14.550959,11.6 Z M13.5,3 C14.3284271,3 15,3.67157288 15,4.5 C15,5.32842712 14.3284271,6 13.5,6 C12.6715729,6 12,5.32842712 12,4.5 C12,3.67157288 12.6715729,3 13.5,3 Z M0,1.77635684e-15 L0,18 L18,18 L18,1.77635684e-15 L0,1.77635684e-15 Z M16,16 L2,16 L2,2 L16,2 L16,16 Z" id="Shape"></path>
+        </g>
+    </g>
+</svg>

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/texture_edit.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2a963be55d5e0a339a9ec137972c8f3455a1cf9d7f4b3b9664ce338bcb0c39bf
+size 227

+ 14 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/toneMapping.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background: #444444;">
+    <!-- Generator: Sketch 64 (93537) - https://sketch.com -->
+    <title>Icons / Toolbar / Tone Mapping</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Icons-/-Toolbar-/-Tone-Mapping" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <rect fill="#444444" x="0" y="0" width="24" height="24"></rect>
+        <rect id="Icon-Background" x="0" y="0" width="24" height="24"></rect>
+        <path d="M21,3 L21,21 L3,21 L3,3 L21,3 Z M19,5 L5,5 L5,19 L19,19 L19,5 Z" id="Combined-Shape" fill="#FFFFFF"></path>
+        <path d="M4.5,19.5 C7.10977381,19.3752859 9.2645953,18.2113806 10.9644645,16.0082842 C13.5142682,12.7036395 12.2106423,9.58363419 14.3305826,7 C15.7438762,5.27757721 17.4670153,4.44424388 19.5,4.5" id="Line-2" stroke="#FFFFFF" stroke-width="2" stroke-linecap="square"></path>
+        <rect id="Rectangle" fill="#FFFFFF" x="5" y="3" width="16" height="2"></rect>
+        <rect id="Rectangle" fill="#FFFFFF" x="19" y="3" width="2" height="16"></rect>
+    </g>
+</svg>

+ 3 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Icons/up.png

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:74278b5d4f4786a2a49f575c22b2afc5bac150e5e8ea3b2ef50ca5b37bdfc9e1
+size 341

+ 235 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Inspector/MaterialCanvasInspector.cpp

@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/RPI.Edit/Common/AssetUtils.h>
+#include <Atom/RPI.Edit/Material/MaterialPropertyId.h>
+#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
+#include <Atom/RPI.Edit/Material/MaterialUtils.h>
+#include <AtomToolsFramework/Document/AtomToolsDocumentRequestBus.h>
+#include <AtomToolsFramework/DynamicProperty/DynamicPropertyGroup.h>
+#include <AtomToolsFramework/Inspector/InspectorPropertyGroupWidget.h>
+#include <AtomToolsFramework/Util/MaterialPropertyUtil.h>
+#include <Document/MaterialCanvasDocumentRequestBus.h>
+#include <Window/Inspector/MaterialCanvasInspector.h>
+
+namespace MaterialCanvas
+{
+    MaterialCanvasInspector::MaterialCanvasInspector(QWidget* parent)
+        : AtomToolsFramework::InspectorWidget(parent)
+    {
+        m_windowSettings = AZ::UserSettings::CreateFind<MaterialCanvasMainWindowSettings>(
+            AZ::Crc32("MaterialCanvasMainWindowSettings"), AZ::UserSettings::CT_GLOBAL);
+
+        AtomToolsFramework::AtomToolsDocumentNotificationBus::Handler::BusConnect();
+    }
+
+    MaterialCanvasInspector::~MaterialCanvasInspector()
+    {
+        AtomToolsFramework::AtomToolsDocumentNotificationBus::Handler::BusDisconnect();
+        AtomToolsFramework::InspectorRequestBus::Handler::BusDisconnect();
+    }
+
+    void MaterialCanvasInspector::Reset()
+    {
+        m_documentPath.clear();
+        m_documentId = AZ::Uuid::CreateNull();
+        m_groups = {};
+
+        AtomToolsFramework::InspectorRequestBus::Handler::BusDisconnect();
+        AtomToolsFramework::InspectorWidget::Reset();
+    }
+
+    bool MaterialCanvasInspector::ShouldGroupAutoExpanded(const AZStd::string& groupName) const
+    {
+        auto stateItr = m_windowSettings->m_inspectorCollapsedGroups.find(GetGroupSaveStateKey(groupName));
+        return stateItr == m_windowSettings->m_inspectorCollapsedGroups.end();
+    }
+
+    void MaterialCanvasInspector::OnGroupExpanded(const AZStd::string& groupName)
+    {
+        m_windowSettings->m_inspectorCollapsedGroups.erase(GetGroupSaveStateKey(groupName));
+    }
+
+    void MaterialCanvasInspector::OnGroupCollapsed(const AZStd::string& groupName)
+    {
+        m_windowSettings->m_inspectorCollapsedGroups.insert(GetGroupSaveStateKey(groupName));
+    }
+
+    void MaterialCanvasInspector::OnDocumentOpened(const AZ::Uuid& documentId)
+    {
+        AddGroupsBegin();
+
+        m_documentId = documentId;
+
+        bool isOpen = false;
+        AtomToolsFramework::AtomToolsDocumentRequestBus::EventResult(isOpen, m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::IsOpen);
+
+        AtomToolsFramework::AtomToolsDocumentRequestBus::EventResult(m_documentPath, m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::GetAbsolutePath);
+
+        if (!m_documentId.IsNull() && isOpen)
+        {
+            // Create the top group for displaying overview info about the material
+            AddOverviewGroup();
+            // Create groups for displaying editable properties
+            AddPropertiesGroup();
+
+            AtomToolsFramework::InspectorRequestBus::Handler::BusConnect(m_documentId);
+        }
+
+        AddGroupsEnd();
+    }
+
+    AZ::Crc32 MaterialCanvasInspector::GetGroupSaveStateKey(const AZStd::string& groupName) const
+    {
+        return AZ::Crc32(AZStd::string::format("MaterialCanvasInspector::PropertyGroup::%s::%s", m_documentPath.c_str(), groupName.c_str()));
+    }
+
+    bool MaterialCanvasInspector::IsInstanceNodePropertyModifed(const AzToolsFramework::InstanceDataNode* node) const
+    {
+        const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(node);
+        return property && !AtomToolsFramework::ArePropertyValuesEqual(property->GetValue(), property->GetConfig().m_parentValue);
+    }
+
+    const char* MaterialCanvasInspector::GetInstanceNodePropertyIndicator(const AzToolsFramework::InstanceDataNode* node) const
+    {
+        if (IsInstanceNodePropertyModifed(node))
+        {
+            return ":/Icons/changed_property.svg";
+        }
+        return ":/Icons/blank.png";
+    }
+
+    void MaterialCanvasInspector::AddOverviewGroup()
+    {
+        const AZStd::string groupName = "overview";
+        const AZStd::string groupDisplayName = "Overview";
+        const AZStd::string groupDescription = "";
+        auto& group = m_groups[groupName];
+
+        AtomToolsFramework::DynamicProperty property;
+        AtomToolsFramework::AtomToolsDocumentRequestBus::EventResult(
+            property, m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::GetProperty, AZ::Name("overview.materialType"));
+        group.m_properties.push_back(property);
+
+        property = {};
+        AtomToolsFramework::AtomToolsDocumentRequestBus::EventResult(
+            property, m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::GetProperty, AZ::Name("overview.parentMaterial"));
+        group.m_properties.push_back(property);
+
+        // Passing in same group as main and comparison instance to enable custom value comparison for highlighting modified properties
+        auto propertyGroupWidget = new AtomToolsFramework::InspectorPropertyGroupWidget(
+            &group, &group, group.TYPEINFO_Uuid(), this, this, GetGroupSaveStateKey(groupName), {},
+            [this](const auto node) { return GetInstanceNodePropertyIndicator(node); }, 0);
+        AddGroup(groupName, groupDisplayName, groupDescription, propertyGroupWidget);
+    }
+
+    void MaterialCanvasInspector::AddPropertiesGroup()
+    {
+    }
+
+    void MaterialCanvasInspector::OnDocumentPropertyValueModified(const AZ::Uuid& documentId, const AtomToolsFramework::DynamicProperty& property)
+    {
+        for (auto& groupPair : m_groups)
+        {
+            for (auto& reflectedProperty : groupPair.second.m_properties)
+            {
+                if (reflectedProperty.GetId() == property.GetId())
+                {
+                    if (!AtomToolsFramework::ArePropertyValuesEqual(reflectedProperty.GetValue(), property.GetValue()))
+                    {
+                        reflectedProperty.SetValue(property.GetValue());
+                        AtomToolsFramework::InspectorRequestBus::Event(
+                            documentId, &AtomToolsFramework::InspectorRequestBus::Events::RefreshGroup, groupPair.first);
+                    }
+                    return;
+                }
+            }
+        }
+    }
+
+    void MaterialCanvasInspector::OnDocumentPropertyConfigModified(const AZ::Uuid&, const AtomToolsFramework::DynamicProperty& property)
+    {
+        for (auto& groupPair : m_groups)
+        {
+            for (auto& reflectedProperty : groupPair.second.m_properties)
+            {
+                if (reflectedProperty.GetId() == property.GetId())
+                {
+                    // Visibility changes require the entire reflected property editor tree for this group to be rebuilt
+                    if (reflectedProperty.GetVisibility() != property.GetVisibility())
+                    {
+                        reflectedProperty.SetConfig(property.GetConfig());
+                        RebuildGroup(groupPair.first);
+                    }
+                    else
+                    {
+                        reflectedProperty.SetConfig(property.GetConfig());
+                        RefreshGroup(groupPair.first);
+                    }
+                    return;
+                }
+            }
+        }
+    }
+    
+    void MaterialCanvasInspector::OnDocumentPropertyGroupVisibilityChanged(const AZ::Uuid&, const AZ::Name& groupId, bool visible)
+    {
+        SetGroupVisible(groupId.GetStringView(), visible);
+    }
+
+    void MaterialCanvasInspector::BeforePropertyModified(AzToolsFramework::InstanceDataNode* pNode)
+    {
+        // For some reason the reflected property editor notifications are not symmetrical
+        // This function is called continuously anytime a property changes until the edit has completed
+        // Because of that, we have to track whether or not we are continuing to edit the same property to know when editing has started and
+        // ended
+        const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(pNode);
+        if (property)
+        {
+            if (m_activeProperty != property)
+            {
+                m_activeProperty = property;
+                AtomToolsFramework::AtomToolsDocumentRequestBus::Event(m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::BeginEdit);
+            }
+        }
+    }
+
+    void MaterialCanvasInspector::AfterPropertyModified(AzToolsFramework::InstanceDataNode* pNode)
+    {
+        const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(pNode);
+        if (property)
+        {
+            if (m_activeProperty == property)
+            {
+                AtomToolsFramework::AtomToolsDocumentRequestBus::Event(
+                    m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::SetPropertyValue, property->GetId(), property->GetValue());
+            }
+        }
+    }
+
+    void MaterialCanvasInspector::SetPropertyEditingComplete(AzToolsFramework::InstanceDataNode* pNode)
+    {
+        // As above, there are symmetrical functions on the notification interface for when editing begins and ends and has been completed
+        // but they are not being called following that pattern. when this function executes the changes to the property are ready to be
+        // committed or reverted
+        const AtomToolsFramework::DynamicProperty* property = AtomToolsFramework::FindDynamicPropertyForInstanceDataNode(pNode);
+        if (property)
+        {
+            if (m_activeProperty == property)
+            {
+                AtomToolsFramework::AtomToolsDocumentRequestBus::Event(
+                    m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::SetPropertyValue, property->GetId(), property->GetValue());
+
+                AtomToolsFramework::AtomToolsDocumentRequestBus::Event(m_documentId, &AtomToolsFramework::AtomToolsDocumentRequestBus::Events::EndEdit);
+                m_activeProperty = nullptr;
+            }
+        }
+    }
+} // namespace MaterialCanvas
+
+#include <Window/Inspector/moc_MaterialCanvasInspector.cpp>

+ 74 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/Inspector/MaterialCanvasInspector.h

@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <AtomToolsFramework/Document/AtomToolsDocumentNotificationBus.h>
+#include <AtomToolsFramework/DynamicProperty/DynamicPropertyGroup.h>
+#include <AtomToolsFramework/Inspector/InspectorWidget.h>
+#include <AzCore/std/containers/unordered_map.h>
+#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI_Internals.h>
+#include <Window/MaterialCanvasMainWindowSettings.h>
+#endif
+
+namespace MaterialCanvas
+{
+    //! Provides controls for viewing and editing document settings.
+    class MaterialCanvasInspector
+        : public AtomToolsFramework::InspectorWidget
+        , public AtomToolsFramework::AtomToolsDocumentNotificationBus::Handler
+        , public AzToolsFramework::IPropertyEditorNotify
+    {
+        Q_OBJECT
+    public:
+        AZ_CLASS_ALLOCATOR(MaterialCanvasInspector, AZ::SystemAllocator, 0);
+
+        explicit MaterialCanvasInspector(QWidget* parent = nullptr);
+        ~MaterialCanvasInspector() override;
+
+        // AtomToolsFramework::InspectorRequestBus::Handler overrides...
+        void Reset() override;
+
+    protected:
+        bool ShouldGroupAutoExpanded(const AZStd::string& groupName) const override;
+        void OnGroupExpanded(const AZStd::string& groupName) override;
+        void OnGroupCollapsed(const AZStd::string& groupName) override;
+
+    private:
+        AZ::Crc32 GetGroupSaveStateKey(const AZStd::string& groupName) const;
+        bool IsInstanceNodePropertyModifed(const AzToolsFramework::InstanceDataNode* node) const;
+        const char* GetInstanceNodePropertyIndicator(const AzToolsFramework::InstanceDataNode* node) const;
+
+        void AddOverviewGroup();
+        void AddPropertiesGroup();
+
+        // AtomToolsDocumentNotificationBus::Handler implementation
+        void OnDocumentOpened(const AZ::Uuid& documentId) override;
+        void OnDocumentPropertyValueModified(const AZ::Uuid& documentId, const AtomToolsFramework::DynamicProperty& property) override;
+        void OnDocumentPropertyConfigModified(const AZ::Uuid& documentId, const AtomToolsFramework::DynamicProperty& property) override;
+        void OnDocumentPropertyGroupVisibilityChanged(const AZ::Uuid& documentId, const AZ::Name& groupId, bool visible) override;
+
+        // AzToolsFramework::IPropertyEditorNotify overrides...
+        void BeforePropertyModified(AzToolsFramework::InstanceDataNode* pNode) override;
+        void AfterPropertyModified(AzToolsFramework::InstanceDataNode* pNode) override;
+        void SetPropertyEditingActive([[maybe_unused]] AzToolsFramework::InstanceDataNode* pNode) override {}
+        void SetPropertyEditingComplete(AzToolsFramework::InstanceDataNode* pNode) override;
+        void SealUndoStack() override {}
+        void RequestPropertyContextMenu(AzToolsFramework::InstanceDataNode*, const QPoint&) override {}
+        void PropertySelectionChanged(AzToolsFramework::InstanceDataNode*, bool) override {}
+
+        // Tracking the property that is activiley being edited in the inspector
+        const AtomToolsFramework::DynamicProperty* m_activeProperty = nullptr;
+
+        AZ::Uuid m_documentId = AZ::Uuid::CreateNull();
+        AZStd::string m_documentPath;
+        AZStd::unordered_map<AZStd::string, AtomToolsFramework::DynamicPropertyGroup> m_groups;
+        AZStd::intrusive_ptr<MaterialCanvasMainWindowSettings> m_windowSettings;
+    };
+} // namespace MaterialCanvas

+ 25 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvas.qrc

@@ -0,0 +1,25 @@
+<RCC>
+    <qresource prefix="/">
+        <file>MaterialCanvas.qss</file>
+        <file>Icons/materialcanvas.svg</file>
+        <file>Icons/material.svg</file>
+        <file>Icons/materialtype.svg</file>
+        <file>Icons/mesh.svg</file>
+        <file>Icons/texture.svg</file>
+        <file>Icons/add.png</file>
+        <file>Icons/addnew.png</file>
+        <file>Icons/down.png</file>
+        <file>Icons/up.png</file>
+        <file>Icons/refresh.png</file>
+        <file>Icons/remove.png</file>
+        <file>Icons/save.png</file>
+        <file>Icons/save_active.png</file>
+        <file>Icons/save_disabled.png</file>
+        <file>Icons/save_normal.png</file>
+        <file>Icons/texture_edit.png</file>
+        <file>Icons/grid.svg</file>
+        <file>Icons/shadow.svg</file>
+        <file>Icons/skybox.svg</file>
+        <file>Icons/toneMapping.svg</file>
+    </qresource>
+</RCC>

+ 14 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvas.qss

@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+/* Style for visualizing property values overridden from their prefab values */
+AzToolsFramework--PropertyRowWidget[IsOverridden="true"] QLabel
+{
+    font-weight: bold;
+    color: #1E70EB;
+}

+ 353 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasBrowserInteractions.cpp

@@ -0,0 +1,353 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/RPI.Edit/Common/AssetUtils.h>
+#include <Atom/RPI.Edit/Material/MaterialSourceData.h>
+#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <AtomToolsFramework/Document/AtomToolsDocumentSystemRequestBus.h>
+#include <AtomToolsFramework/Util/Util.h>
+#include <AzCore/Utils/Utils.h>
+#include <AzCore/std/string/wildcard.h>
+#include <AzQtComponents/Utilities/DesktopUtilities.h>
+#include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
+#include <AzToolsFramework/AssetBrowser/AssetBrowserEntry.h>
+#include <AzToolsFramework/AssetBrowser/AssetSelectionModel.h>
+#include <AzToolsFramework/Thumbnails/SourceControlThumbnail.h>
+#include <Window/MaterialCanvasBrowserInteractions.h>
+
+#include <QApplication>
+#include <QClipboard>
+#include <QDesktopServices>
+#include <QFileDialog>
+#include <QInputDialog>
+#include <QMenu>
+#include <QMessageBox>
+
+namespace MaterialCanvas
+{
+    MaterialCanvasBrowserInteractions::MaterialCanvasBrowserInteractions()
+    {
+        using namespace AzToolsFramework::AssetBrowser;
+
+        AssetBrowserInteractionNotificationBus::Handler::BusConnect();
+    }
+
+    MaterialCanvasBrowserInteractions::~MaterialCanvasBrowserInteractions()
+    {
+        AssetBrowserInteractionNotificationBus::Handler::BusDisconnect();
+    }
+
+    void MaterialCanvasBrowserInteractions::AddContextMenuActions(QWidget* caller, QMenu* menu, const AZStd::vector<AzToolsFramework::AssetBrowser::AssetBrowserEntry*>& entries)
+    {
+        AssetBrowserEntry* entry = entries.empty() ? nullptr : entries.front();
+        if (!entry)
+        {
+            return;
+        }
+
+        m_caller = caller;
+        QObject::connect(m_caller, &QObject::destroyed, [this]()
+            {
+                m_caller = nullptr;
+            });
+
+        AddGenericContextMenuActions(caller, menu, entry);
+
+        if (entry->GetEntryType() == AssetBrowserEntry::AssetEntryType::Source)
+        {
+            const auto source = azalias_cast<const SourceAssetBrowserEntry*>(entry);
+            if (AzFramework::StringFunc::Path::IsExtension(entry->GetFullPath().c_str(), AZ::RPI::MaterialSourceData::Extension))
+            {
+                AddContextMenuActionsForMaterialSource(caller, menu, source);
+            }
+            else if (AzFramework::StringFunc::Path::IsExtension(entry->GetFullPath().c_str(), AZ::RPI::MaterialTypeSourceData::Extension))
+            {
+                AddContextMenuActionsForMaterialTypeSource(caller, menu, source);
+            }
+            else
+            {
+                AddContextMenuActionsForOtherSource(caller, menu, source);
+            }
+        }
+        else if (entry->GetEntryType() == AssetBrowserEntry::AssetEntryType::Folder)
+        {
+            const auto folder = azalias_cast<const FolderAssetBrowserEntry*>(entry);
+            AddContextMenuActionsForFolder(caller, menu, folder);
+        }
+    }
+    
+    void MaterialCanvasBrowserInteractions::AddGenericContextMenuActions([[maybe_unused]] QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::AssetBrowserEntry* entry)
+    {
+        menu->addAction(QObject::tr("Copy Name To Clipboard"), [=]()
+            {
+                QApplication::clipboard()->setText(entry->GetName().c_str());
+            });
+        menu->addAction(QObject::tr("Copy Path To Clipboard"), [=]()
+            {
+                QApplication::clipboard()->setText(entry->GetFullPath().c_str());
+            });
+    }
+
+    void MaterialCanvasBrowserInteractions::AddContextMenuActionsForMaterialTypeSource(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* entry)
+    {
+        menu->addAction(AzQtComponents::fileBrowserActionName(), [entry]()
+            {
+                AzQtComponents::ShowFileOnDesktop(entry->GetFullPath().c_str());
+            });
+
+        menu->addSeparator();
+
+        menu->addAction("Create Material...", [entry]()
+            {
+                const QString defaultPath = AtomToolsFramework::GetUniqueFileInfo(
+                    QString(AZ::Utils::GetProjectPath().c_str()) +
+                    AZ_CORRECT_FILESYSTEM_SEPARATOR + "Assets" +
+                    AZ_CORRECT_FILESYSTEM_SEPARATOR + "untitled." +
+                    AZ::RPI::MaterialSourceData::Extension).absoluteFilePath();
+
+                AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Events::CreateDocumentFromFile,
+                    entry->GetFullPath(), AtomToolsFramework::GetSaveFileInfo(defaultPath).absoluteFilePath().toUtf8().constData());
+            });
+
+        AddPerforceMenuActions(caller, menu, entry);
+    }
+
+    void MaterialCanvasBrowserInteractions::AddContextMenuActionsForOtherSource(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* entry)
+    {
+        menu->addAction("Open", [entry]()
+            {
+                QDesktopServices::openUrl(QUrl::fromLocalFile(entry->GetFullPath().c_str()));
+            });
+
+        menu->addAction("Duplicate...", [entry]()
+            {
+                const QFileInfo duplicateFileInfo(AtomToolsFramework::GetDuplicationFileInfo(entry->GetFullPath().c_str()));
+                if (!duplicateFileInfo.absoluteFilePath().isEmpty())
+                {
+                    if (QFile::copy(entry->GetFullPath().c_str(), duplicateFileInfo.absoluteFilePath()))
+                    {
+                        QFile::setPermissions(duplicateFileInfo.absoluteFilePath(), QFile::ReadOther | QFile::WriteOther);
+
+                        // Auto add file to source control
+                        AzToolsFramework::SourceControlCommandBus::Broadcast(&AzToolsFramework::SourceControlCommandBus::Events::RequestEdit,
+                            duplicateFileInfo.absoluteFilePath().toUtf8().constData(), true, [](bool, const AzToolsFramework::SourceControlFileInfo&) {});
+                    }
+                }
+            });
+
+        menu->addAction(AzQtComponents::fileBrowserActionName(), [entry]()
+            {
+                AzQtComponents::ShowFileOnDesktop(entry->GetFullPath().c_str());
+            });
+
+        AddPerforceMenuActions(caller, menu, entry);
+    }
+
+    void MaterialCanvasBrowserInteractions::AddContextMenuActionsForMaterialSource(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* entry)
+    {
+        menu->addAction("Open", [entry]()
+            {
+                AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Events::OpenDocument, entry->GetFullPath());
+            });
+
+        menu->addAction("Duplicate...", [entry]()
+            {
+                const QFileInfo duplicateFileInfo(AtomToolsFramework::GetDuplicationFileInfo(entry->GetFullPath().c_str()));
+                if (!duplicateFileInfo.absoluteFilePath().isEmpty())
+                {
+                    if (QFile::copy(entry->GetFullPath().c_str(), duplicateFileInfo.absoluteFilePath()))
+                    {
+                        QFile::setPermissions(duplicateFileInfo.absoluteFilePath(), QFile::ReadOther | QFile::WriteOther);
+
+                        // Auto add file to source control
+                        AzToolsFramework::SourceControlCommandBus::Broadcast(&AzToolsFramework::SourceControlCommandBus::Events::RequestEdit,
+                            duplicateFileInfo.absoluteFilePath().toUtf8().constData(), true, [](bool, const AzToolsFramework::SourceControlFileInfo&) {});
+                    }
+                }
+            });
+
+        menu->addAction(AzQtComponents::fileBrowserActionName(), [entry]()
+            {
+                AzQtComponents::ShowFileOnDesktop(entry->GetFullPath().c_str());
+            });
+
+        menu->addSeparator();
+
+        menu->addAction("Create Child Material...", [entry]()
+            {
+                const QString defaultPath = AtomToolsFramework::GetUniqueFileInfo(
+                    QString(AZ::Utils::GetProjectPath().c_str()) +
+                    AZ_CORRECT_FILESYSTEM_SEPARATOR + "Assets" +
+                    AZ_CORRECT_FILESYSTEM_SEPARATOR + "untitled." +
+                    AZ::RPI::MaterialSourceData::Extension).absoluteFilePath();
+
+                AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Events::CreateDocumentFromFile,
+                    entry->GetFullPath(), AtomToolsFramework::GetSaveFileInfo(defaultPath).absoluteFilePath().toUtf8().constData());
+            });
+
+        menu->addSeparator();
+
+        QAction* openParentAction = menu->addAction("Open Parent Material", [entry]()
+            {
+                AZ_UNUSED(entry);
+                // ToDo
+            });
+        openParentAction->setEnabled(false);
+
+        AddPerforceMenuActions(caller, menu, entry);
+    }
+
+    void MaterialCanvasBrowserInteractions::AddContextMenuActionsForFolder(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::FolderAssetBrowserEntry* entry)
+    {
+        menu->addAction(AzQtComponents::fileBrowserActionName(), [entry]()
+            {
+                AzQtComponents::ShowFileOnDesktop(entry->GetFullPath().c_str());
+            });
+
+        QAction* createFolderAction = menu->addAction(QObject::tr("Create new sub folder..."));
+        QObject::connect(createFolderAction, &QAction::triggered, caller, [caller, entry]()
+            {
+                bool ok;
+                QString newFolderName = QInputDialog::getText(caller, "Enter new folder name", "name:", QLineEdit::Normal, "NewFolder", &ok);
+                if (ok)
+                {
+                    if (newFolderName.isEmpty())
+                    {
+                        QMessageBox msgBox(QMessageBox::Icon::Critical, "Error", "Folder name can't be empty", QMessageBox::Ok, caller);
+                        msgBox.exec();
+                    }
+                    else
+                    {
+                        AZStd::string newFolderPath;
+                        AzFramework::StringFunc::Path::Join(entry->GetFullPath().c_str(), newFolderName.toUtf8().constData(), newFolderPath);
+                        QDir dir(newFolderPath.c_str());
+                        if (dir.exists())
+                        {
+                            QMessageBox::critical(caller, "Error", "Folder with this name already exists");
+                            return;
+                        }
+                        auto result = dir.mkdir(newFolderPath.c_str());
+                        if (!result)
+                        {
+                            AZ_Error("MaterialBrowser", false, "Failed to make new folder");
+                            return;
+                        }
+                    }
+                }
+            });
+
+        menu->addSeparator();
+    }
+
+    void MaterialCanvasBrowserInteractions::AddPerforceMenuActions([[maybe_unused]] QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::AssetBrowserEntry* entry)
+    {
+        using namespace AzToolsFramework;
+
+        bool isActive = false;
+        SourceControlConnectionRequestBus::BroadcastResult(isActive, &SourceControlConnectionRequests::IsActive);
+
+        if (isActive)
+        {
+            menu->addSeparator();
+
+            AZStd::string path = entry->GetFullPath();
+            AzFramework::StringFunc::Path::Normalize(path);
+
+            QMenu* sourceControlMenu = menu->addMenu("Source Control");
+
+            // Update the enabled state of source control menu actions only if menu is shown
+            QMenu::connect(sourceControlMenu, &QMenu::aboutToShow, [this, path]()
+                {
+                    SourceControlCommandBus::Broadcast(&SourceControlCommandBus::Events::GetFileInfo, path.c_str(),
+                        [this](bool success, const SourceControlFileInfo& info) { UpdateSourceControlActions(success, info); });
+                });
+
+            // add get latest action
+            m_getLatestAction = sourceControlMenu->addAction("Get Latest", [path]()
+                {
+                    SourceControlCommandBus::Broadcast(&SourceControlCommandBus::Events::RequestLatest, path.c_str(),
+                        [](bool, const SourceControlFileInfo&) {});
+                });
+            QObject::connect(m_getLatestAction, &QObject::destroyed, [this]()
+                {
+                    m_getLatestAction = nullptr;
+                });
+            m_getLatestAction->setEnabled(false);
+
+            // add add action
+            m_addAction = sourceControlMenu->addAction("Add", [path]()
+                {
+                    SourceControlCommandBus::Broadcast(&SourceControlCommandBus::Events::RequestEdit, path.c_str(), true,
+                        [path](bool, const SourceControlFileInfo&)
+                        {
+                            SourceControlThumbnailRequestBus::Broadcast(&SourceControlThumbnailRequests::FileStatusChanged, path.c_str());
+                        });
+                });
+            QObject::connect(m_addAction, &QObject::destroyed, [this]()
+                {
+                    m_addAction = nullptr;
+                });
+            m_addAction->setEnabled(false);
+
+            // add checkout action
+            m_checkOutAction = sourceControlMenu->addAction("Check Out", [path]()
+                {
+                    SourceControlCommandBus::Broadcast(&SourceControlCommandBus::Events::RequestEdit, path.c_str(), true,
+                        [path](bool, const SourceControlFileInfo&)
+                        {
+                            SourceControlThumbnailRequestBus::Broadcast(&SourceControlThumbnailRequests::FileStatusChanged, path.c_str());
+                        });
+                });
+            QObject::connect(m_checkOutAction, &QObject::destroyed, [this]()
+                {
+                    m_checkOutAction = nullptr;
+                });
+            m_checkOutAction->setEnabled(false);
+
+            // add undo checkout action
+            m_undoCheckOutAction = sourceControlMenu->addAction("Undo Check Out", [path]()
+                {
+                    SourceControlCommandBus::Broadcast(&SourceControlCommandBus::Events::RequestRevert, path.c_str(),
+                        [path](bool, const SourceControlFileInfo&)
+                        {
+                            SourceControlThumbnailRequestBus::Broadcast(&SourceControlThumbnailRequests::FileStatusChanged, path.c_str());
+                        });
+                });
+            QObject::connect(m_undoCheckOutAction, &QObject::destroyed, [this]()
+                {
+                    m_undoCheckOutAction = nullptr;
+                });
+            m_undoCheckOutAction->setEnabled(false);
+        }
+    }
+
+    void MaterialCanvasBrowserInteractions::UpdateSourceControlActions(bool success, AzToolsFramework::SourceControlFileInfo info)
+    {
+        if (!success && m_caller)
+        {
+            QMessageBox::critical(m_caller, "Error", "Source control operation failed.");
+        }
+        if (m_getLatestAction)
+        {
+            m_getLatestAction->setEnabled(info.IsManaged() && info.HasFlag(AzToolsFramework::SCF_OutOfDate));
+        }
+        if (m_addAction)
+        {
+            m_addAction->setEnabled(!info.IsManaged());
+        }
+        if (m_checkOutAction)
+        {
+            m_checkOutAction->setEnabled(info.IsManaged() && info.IsReadOnly() && !info.IsLockedByOther());
+        }
+        if (m_undoCheckOutAction)
+        {
+            m_undoCheckOutAction->setEnabled(info.IsManaged() && !info.IsReadOnly());
+        }
+    }
+} // namespace MaterialCanvas

+ 58 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasBrowserInteractions.h

@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
+#include <AzToolsFramework/SourceControl/SourceControlAPI.h>
+
+class QWidget;
+class QMenu;
+class QAction;
+
+namespace AzToolsFramework
+{
+    namespace AssetBrowser
+    {
+        class AssetBrowserEntry;
+        class SourceAssetBrowserEntry;
+        class FolderAssetBrowserEntry;
+    }
+}
+
+namespace MaterialCanvas
+{
+    class MaterialCanvasBrowserInteractions
+        : public AzToolsFramework::AssetBrowser::AssetBrowserInteractionNotificationBus::Handler
+    {
+    public:
+        AZ_CLASS_ALLOCATOR(MaterialCanvasBrowserInteractions, AZ::SystemAllocator, 0);
+
+        MaterialCanvasBrowserInteractions();
+        ~MaterialCanvasBrowserInteractions();
+
+    private:
+        //! AssetBrowserInteractionNotificationBus::Handler overrides...
+        void AddContextMenuActions(QWidget* caller, QMenu* menu, const AZStd::vector<AzToolsFramework::AssetBrowser::AssetBrowserEntry*>& entries) override;
+
+        void AddGenericContextMenuActions(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::AssetBrowserEntry* entry);
+        void AddContextMenuActionsForOtherSource(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* entry);
+        void AddContextMenuActionsForMaterialSource(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* entry);
+        void AddContextMenuActionsForMaterialTypeSource(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* entry);
+        void AddContextMenuActionsForFolder(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::FolderAssetBrowserEntry* entry);
+        void AddPerforceMenuActions(QWidget* caller, QMenu* menu, const AzToolsFramework::AssetBrowser::AssetBrowserEntry* entry);
+
+        void UpdateSourceControlActions(bool success, AzToolsFramework::SourceControlFileInfo info);
+
+        QWidget* m_caller = nullptr;
+        QAction* m_addAction = nullptr;
+        QAction* m_checkOutAction = nullptr;
+        QAction* m_undoCheckOutAction = nullptr;
+        QAction* m_getLatestAction = nullptr;
+    };
+} // namespace MaterialCanvas

+ 190 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindow.cpp

@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/RHI/Factory.h>
+#include <Atom/RPI.Edit/Material/MaterialSourceData.h>
+#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
+#include <AtomToolsFramework/Document/AtomToolsDocumentSystemRequestBus.h>
+#include <AtomToolsFramework/Util/Util.h>
+#include <AzQtComponents/Components/StyleManager.h>
+#include <AzQtComponents/Components/WindowDecorationWrapper.h>
+#include <Document/MaterialCanvasDocumentRequestBus.h>
+#include <Viewport/MaterialCanvasViewportWidget.h>
+#include <Window/MaterialCanvasMainWindow.h>
+#include <Window/Inspector/MaterialCanvasInspector.h>
+
+AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
+#include <QApplication>
+#include <QByteArray>
+#include <QCloseEvent>
+#include <QDesktopServices>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QStatusBar>
+#include <QUrl>
+#include <QWindow>
+AZ_POP_DISABLE_WARNING
+
+namespace MaterialCanvas
+{
+    MaterialCanvasMainWindow::MaterialCanvasMainWindow(QWidget* parent /* = 0 */)
+        : Base(parent)
+    {
+        resize(1280, 1024);
+
+        // Among other things, we need the window wrapper to save the main window size, position, and state
+        auto mainWindowWrapper =
+            new AzQtComponents::WindowDecorationWrapper(AzQtComponents::WindowDecorationWrapper::OptionAutoTitleBarButtons);
+        mainWindowWrapper->setGuest(this);
+        mainWindowWrapper->enableSaveRestoreGeometry("O3DE", "MaterialCanvas", "mainWindowGeometry");
+
+        // set the style sheet for RPE highlighting and other styling
+        AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral(":/MaterialCanvas.qss"));
+
+        QApplication::setWindowIcon(QIcon(":/Icons/materialcanvas.svg"));
+
+        AZ::Name apiName = AZ::RHI::Factory::Get().GetName();
+        if (!apiName.IsEmpty())
+        {
+            QString title = QString{ "%1 (%2)" }.arg(QApplication::applicationName()).arg(apiName.GetCStr());
+            setWindowTitle(title);
+        }
+        else
+        {
+            AZ_Assert(false, "Render API name not found");
+            setWindowTitle(QApplication::applicationName());
+        }
+
+        setObjectName("MaterialCanvasMainWindow");
+
+        m_toolBar = new MaterialCanvasToolBar(this);
+        m_toolBar->setObjectName("ToolBar");
+        addToolBar(m_toolBar);
+
+        m_materialCanvasViewport = new MaterialCanvasViewportWidget(centralWidget());
+        m_materialCanvasViewport->setObjectName("Viewport");
+        m_materialCanvasViewport->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+        centralWidget()->layout()->addWidget(m_materialCanvasViewport);
+
+        m_assetBrowser->SetFilterState("", AZ::RPI::StreamingImageAsset::Group, true);
+        m_assetBrowser->SetFilterState("", AZ::RPI::MaterialAsset::Group, true);
+        m_assetBrowser->SetOpenHandler([](const AZStd::string& absolutePath) {
+            if (AzFramework::StringFunc::Path::IsExtension(absolutePath.c_str(), AZ::RPI::MaterialSourceData::Extension))
+            {
+                AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Broadcast(
+                    &AtomToolsFramework::AtomToolsDocumentSystemRequestBus::Events::OpenDocument, absolutePath);
+                return;
+            }
+
+            if (AzFramework::StringFunc::Path::IsExtension(absolutePath.c_str(), AZ::RPI::MaterialTypeSourceData::Extension))
+            {
+                return;
+            }
+
+            QDesktopServices::openUrl(QUrl::fromLocalFile(absolutePath.c_str()));
+        });
+
+        AddDockWidget("Inspector", new MaterialCanvasInspector, Qt::RightDockWidgetArea, Qt::Vertical);
+
+        // Restore geometry and show the window
+        mainWindowWrapper->showFromSettings();
+
+        // Restore additional state for docked windows
+        auto windowSettings = AZ::UserSettings::CreateFind<MaterialCanvasMainWindowSettings>(
+            AZ::Crc32("MaterialCanvasMainWindowSettings"), AZ::UserSettings::CT_GLOBAL);
+
+        if (!windowSettings->m_mainWindowState.empty())
+        {
+            QByteArray windowState(windowSettings->m_mainWindowState.data(), static_cast<int>(windowSettings->m_mainWindowState.size()));
+            m_advancedDockManager->restoreState(windowState);
+        }
+
+        OnDocumentOpened(AZ::Uuid::CreateNull());
+    }
+
+    void MaterialCanvasMainWindow::ResizeViewportRenderTarget(uint32_t width, uint32_t height)
+    {
+        QSize requestedViewportSize = QSize(width, height) / devicePixelRatioF();
+        QSize currentViewportSize = m_materialCanvasViewport->size();
+        QSize offset = requestedViewportSize - currentViewportSize;
+        QSize requestedWindowSize = size() + offset;
+        resize(requestedWindowSize);
+
+        AZ_Assert(
+            m_materialCanvasViewport->size() == requestedViewportSize,
+            "Resizing the window did not give the expected viewport size. Requested %d x %d but got %d x %d.",
+            requestedViewportSize.width(), requestedViewportSize.height(), m_materialCanvasViewport->size().width(),
+            m_materialCanvasViewport->size().height());
+
+        [[maybe_unused]] QSize newDeviceSize = m_materialCanvasViewport->size();
+        AZ_Warning(
+            "Material Canvas", static_cast<uint32_t>(newDeviceSize.width()) == width && static_cast<uint32_t>(newDeviceSize.height()) == height,
+            "Resizing the window did not give the expected frame size. Requested %d x %d but got %d x %d.", width, height,
+            newDeviceSize.width(), newDeviceSize.height());
+    }
+
+    void MaterialCanvasMainWindow::LockViewportRenderTargetSize(uint32_t width, uint32_t height)
+    {
+        m_materialCanvasViewport->LockRenderTargetSize(width, height);
+    }
+
+    void MaterialCanvasMainWindow::UnlockViewportRenderTargetSize()
+    {
+        m_materialCanvasViewport->UnlockRenderTargetSize();
+    }
+
+    bool MaterialCanvasMainWindow::GetCreateDocumentParams(AZStd::string& /*openPath*/, AZStd::string& /*savePath*/)
+    {
+        return false;
+    }
+
+    bool MaterialCanvasMainWindow::GetOpenDocumentParams(AZStd::string& openPath)
+    {
+        const AZStd::vector<AZ::Data::AssetType> assetTypes = { azrtti_typeid<AZ::RPI::MaterialAsset>() };
+        openPath = AtomToolsFramework::GetOpenFileInfo(assetTypes).absoluteFilePath().toUtf8().constData();
+        return !openPath.empty();
+    }
+
+    void MaterialCanvasMainWindow::OpenSettings()
+    {
+    }
+
+    void MaterialCanvasMainWindow::OpenHelp()
+    {
+        QMessageBox::information(
+            this, windowTitle(),
+            R"(<html><head/><body>
+            <p><h3><u>Material Canvas Controls</u></h3></p>
+            <p><b>LMB</b> - pan camera</p>
+            <p><b>RMB</b> or <b>Alt+LMB</b> - orbit camera around target</p>
+            <p><b>MMB</b> or <b>Alt+MMB</b> - move camera on its xy plane</p>
+            <p><b>Alt+RMB</b> or <b>LMB+RMB</b> - dolly camera on its z axis</p>
+            <p><b>Ctrl+LMB</b> - rotate model</p>
+            <p><b>Shift+LMB</b> - rotate environment</p>
+            </body></html>)");
+    }
+
+    void MaterialCanvasMainWindow::OpenAbout()
+    {
+        QMessageBox::about(this, windowTitle(), QApplication::applicationName());
+    }
+
+    void MaterialCanvasMainWindow::closeEvent(QCloseEvent* closeEvent)
+    {
+        // Capture docking state before shutdown
+        auto windowSettings = AZ::UserSettings::CreateFind<MaterialCanvasMainWindowSettings>(
+            AZ::Crc32("MaterialCanvasMainWindowSettings"), AZ::UserSettings::CT_GLOBAL);
+
+        QByteArray windowState = m_advancedDockManager->saveState();
+        windowSettings->m_mainWindowState.assign(windowState.begin(), windowState.end());
+
+        Base::closeEvent(closeEvent);
+    }
+} // namespace MaterialCanvas
+
+#include <Window/moc_MaterialCanvasMainWindow.cpp>

+ 53 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindow.h

@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <AtomToolsFramework/Document/AtomToolsDocumentMainWindow.h>
+
+AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
+#include <Viewport/MaterialCanvasViewportWidget.h>
+#include <Window/ToolBar/MaterialCanvasToolBar.h>
+AZ_POP_DISABLE_WARNING
+#endif
+
+namespace MaterialCanvas
+{
+    //! MaterialCanvasMainWindow is the main class. Its responsibility is limited to initializing and connecting
+    //! its panels, managing selection of assets, and performing high-level actions like saving. It contains...
+    //! 2) MaterialCanvasViewport        - The user can see the selected Material applied to a model.
+    //! 3) MaterialPropertyInspector  - The user edits the properties of the selected Material.
+    class MaterialCanvasMainWindow
+        : public AtomToolsFramework::AtomToolsDocumentMainWindow
+    {
+        Q_OBJECT
+    public:
+        AZ_CLASS_ALLOCATOR(MaterialCanvasMainWindow, AZ::SystemAllocator, 0);
+
+        using Base = AtomToolsFramework::AtomToolsDocumentMainWindow;
+
+        MaterialCanvasMainWindow(QWidget* parent = 0);
+
+    protected:
+        void ResizeViewportRenderTarget(uint32_t width, uint32_t height) override;
+        void LockViewportRenderTargetSize(uint32_t width, uint32_t height) override;
+        void UnlockViewportRenderTargetSize() override;
+
+        bool GetCreateDocumentParams(AZStd::string& openPath, AZStd::string& savePath) override;
+        bool GetOpenDocumentParams(AZStd::string& openPath) override;
+        void OpenSettings() override;
+        void OpenHelp() override;
+        void OpenAbout() override;
+
+        void closeEvent(QCloseEvent* closeEvent) override;
+
+        MaterialCanvasViewportWidget* m_materialCanvasViewport = {};
+        MaterialCanvasToolBar* m_toolBar = {};
+    };
+} // namespace MaterialCanvas

+ 46 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindowSettings.cpp

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/RTTI/BehaviorContext.h>
+#include <AzCore/Serialization/EditContext.h>
+#include <Window/MaterialCanvasMainWindowSettings.h>
+
+namespace MaterialCanvas
+{
+    void MaterialCanvasMainWindowSettings::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MaterialCanvasMainWindowSettings, AZ::UserSettings>()
+                ->Version(1)
+                ->Field("mainWindowState", &MaterialCanvasMainWindowSettings::m_mainWindowState)
+                ->Field("inspectorCollapsedGroups", &MaterialCanvasMainWindowSettings::m_inspectorCollapsedGroups)
+                ;
+
+            if (auto editContext = serializeContext->GetEditContext())
+            {
+                editContext->Class<MaterialCanvasMainWindowSettings>(
+                    "MaterialCanvasMainWindowSettings", "")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ;
+            }
+        }
+
+        if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->Class<MaterialCanvasMainWindowSettings>("MaterialCanvasMainWindowSettings")
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Editor")
+                ->Attribute(AZ::Script::Attributes::Module, "materialcanvas")
+                ->Constructor()
+                ->Constructor<const MaterialCanvasMainWindowSettings&>()
+                ;
+        }
+    }
+} // namespace MaterialCanvas

+ 32 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/MaterialCanvasMainWindowSettings.h

@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <AzCore/Memory/Memory.h>
+#include <AzCore/RTTI/RTTI.h>
+#include <AzCore/RTTI/ReflectContext.h>
+#include <AzCore/std/containers/unordered_set.h>
+#include <AzCore/UserSettings/UserSettings.h>
+#endif
+
+namespace MaterialCanvas
+{
+    struct MaterialCanvasMainWindowSettings
+        : public AZ::UserSettings
+    {
+        AZ_RTTI(MaterialCanvasMainWindowSettings, "{BB9DEB77-B7BE-4DF5-9FDD-6D9F3136C4EA}", AZ::UserSettings);
+        AZ_CLASS_ALLOCATOR(MaterialCanvasMainWindowSettings, AZ::SystemAllocator, 0);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        AZStd::vector<char> m_mainWindowState;
+        AZStd::unordered_set<AZ::u32> m_inspectorCollapsedGroups;
+    };
+} // namespace MaterialCanvas

+ 105 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/LightingPresetComboBox.cpp

@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/Feature/Utils/LightingPreset.h>
+#include <Viewport/MaterialCanvasViewportRequestBus.h>
+#include <Window/ToolBar/LightingPresetComboBox.h>
+
+namespace MaterialCanvas
+{
+    LightingPresetComboBox::LightingPresetComboBox(QWidget* parent)
+        : QComboBox(parent)
+    {
+        connect(this, static_cast<void(QComboBox::*)(const int)>(&QComboBox::currentIndexChanged), this, [this](int index) {
+            if (index >= 0 && index < m_presets.size())
+            {
+                MaterialCanvasViewportRequestBus::Broadcast(
+                    &MaterialCanvasViewportRequestBus::Events::SelectLightingPreset, m_presets[index]);
+            }
+            });
+
+        Refresh();
+
+        MaterialCanvasViewportNotificationBus::Handler::BusConnect();
+    }
+
+    LightingPresetComboBox::~LightingPresetComboBox()
+    {
+        MaterialCanvasViewportNotificationBus::Handler::BusDisconnect();
+    }
+
+    void LightingPresetComboBox::Refresh()
+    {
+        clear();
+        setDuplicatesEnabled(true);
+
+        m_presets.clear();
+        MaterialCanvasViewportRequestBus::BroadcastResult(m_presets, &MaterialCanvasViewportRequestBus::Events::GetLightingPresets);
+
+        AZStd::sort(m_presets.begin(), m_presets.end(), [](const auto& a, const auto& b) {
+            return a->m_displayName < b->m_displayName; });
+
+        blockSignals(true);
+        for (const auto& preset : m_presets)
+        {
+            addItem(preset->m_displayName.c_str());
+        }
+        blockSignals(false);
+
+        AZ::Render::LightingPresetPtr preset;
+        MaterialCanvasViewportRequestBus::BroadcastResult(preset, &MaterialCanvasViewportRequestBus::Events::GetLightingPresetSelection);
+        OnLightingPresetSelected(preset);
+    }
+
+    void LightingPresetComboBox::OnLightingPresetSelected(AZ::Render::LightingPresetPtr preset)
+    {
+        auto presetItr = AZStd::find(m_presets.begin(), m_presets.end(), preset);
+        if (presetItr != m_presets.end())
+        {
+            setCurrentIndex(static_cast<int>(AZStd::distance(m_presets.begin(), presetItr)));
+        }
+    }
+
+    void LightingPresetComboBox::OnLightingPresetAdded(AZ::Render::LightingPresetPtr preset)
+    {
+        if (!m_reloading)
+        {
+            Refresh();
+        }
+    }
+
+    void LightingPresetComboBox::OnLightingPresetChanged(AZ::Render::LightingPresetPtr preset)
+    {
+        if (!m_reloading)
+        {
+            auto presetItr = AZStd::find(m_presets.begin(), m_presets.end(), preset);
+            if (presetItr != m_presets.end())
+            {
+                setItemText(static_cast<int>(AZStd::distance(m_presets.begin(), presetItr)), preset->m_displayName.c_str());
+            }
+            else
+            {
+                Refresh();
+            }
+        }
+    }
+
+    void LightingPresetComboBox::OnBeginReloadContent()
+    {
+        m_reloading = true;
+    }
+
+    void LightingPresetComboBox::OnEndReloadContent()
+    {
+        m_reloading = false;
+        Refresh();
+    }
+
+} // namespace MaterialCanvas
+
+#include <Window/ToolBar/moc_LightingPresetComboBox.cpp>

+ 38 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/LightingPresetComboBox.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <QComboBox>
+#include <Viewport/MaterialCanvasViewportNotificationBus.h>
+#endif
+
+namespace MaterialCanvas
+{
+    class LightingPresetComboBox
+        : public QComboBox
+        , public MaterialCanvasViewportNotificationBus::Handler
+    {
+        Q_OBJECT
+    public:
+        LightingPresetComboBox(QWidget* parent = 0);
+        ~LightingPresetComboBox();
+        void Refresh();
+    private:
+        // MaterialCanvasViewportNotificationBus::Handler overrides...
+        void OnLightingPresetSelected(AZ::Render::LightingPresetPtr preset) override;
+        void OnLightingPresetAdded(AZ::Render::LightingPresetPtr preset) override;
+        void OnLightingPresetChanged(AZ::Render::LightingPresetPtr preset) override;
+        void OnBeginReloadContent() override;
+        void OnEndReloadContent() override;
+
+        bool m_reloading = false;
+        AZ::Render::LightingPresetPtrVector m_presets;
+    };
+} // namespace MaterialCanvas

+ 136 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/MaterialCanvasToolBar.cpp

@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/std/containers/vector.h>
+#include <Viewport/MaterialCanvasViewportNotificationBus.h>
+#include <Viewport/MaterialCanvasViewportRequestBus.h>
+#include <Viewport/MaterialCanvasViewportSettings.h>
+#include <Window/ToolBar/LightingPresetComboBox.h>
+#include <Window/ToolBar/MaterialCanvasToolBar.h>
+#include <Window/ToolBar/ModelPresetComboBox.h>
+
+AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
+#include <AzQtComponents/Components/Widgets/ToolBar.h>
+#include <QAbstractItemView>
+#include <QAction>
+#include <QIcon>
+#include <QMenu>
+#include <QToolButton>
+AZ_POP_DISABLE_WARNING
+
+namespace MaterialCanvas
+{
+    MaterialCanvasToolBar::MaterialCanvasToolBar(QWidget* parent)
+        : QToolBar(parent)
+    {
+        AzQtComponents::ToolBar::addMainToolBarStyle(this);
+
+        AZStd::intrusive_ptr<MaterialCanvasViewportSettings> viewportSettings =
+            AZ::UserSettings::CreateFind<MaterialCanvasViewportSettings>(AZ::Crc32("MaterialCanvasViewportSettings"), AZ::UserSettings::CT_GLOBAL);
+
+        // Add toggle grid button
+        m_toggleGrid = addAction(QIcon(":/Icons/grid.svg"), "Toggle Grid");
+        m_toggleGrid->setCheckable(true);
+        connect(m_toggleGrid, &QAction::triggered, [this]() {
+            MaterialCanvasViewportRequestBus::Broadcast(&MaterialCanvasViewportRequestBus::Events::SetGridEnabled, m_toggleGrid->isChecked());
+        });
+        m_toggleGrid->setChecked(viewportSettings->m_enableGrid);
+
+        // Add toggle shadow catcher button
+        m_toggleShadowCatcher = addAction(QIcon(":/Icons/shadow.svg"), "Toggle Shadow Catcher");
+        m_toggleShadowCatcher->setCheckable(true);
+        connect(m_toggleShadowCatcher, &QAction::triggered, [this]() {
+            MaterialCanvasViewportRequestBus::Broadcast(
+                &MaterialCanvasViewportRequestBus::Events::SetShadowCatcherEnabled, m_toggleShadowCatcher->isChecked());
+        });
+        m_toggleShadowCatcher->setChecked(viewportSettings->m_enableShadowCatcher);
+
+        // Add toggle alternate skybox button
+        m_toggleAlternateSkybox = addAction(QIcon(":/Icons/skybox.svg"), "Toggle Alternate Skybox");
+        m_toggleAlternateSkybox->setCheckable(true);
+        connect(m_toggleAlternateSkybox, &QAction::triggered, [this]() {
+            MaterialCanvasViewportRequestBus::Broadcast(
+                &MaterialCanvasViewportRequestBus::Events::SetAlternateSkyboxEnabled, m_toggleAlternateSkybox->isChecked());
+        });
+        m_toggleAlternateSkybox->setChecked(viewportSettings->m_enableAlternateSkybox);
+
+        // Add mapping selection button
+        QToolButton* toneMappingButton = new QToolButton(this);
+        QMenu* toneMappingMenu = new QMenu(toneMappingButton);
+
+        m_operationNames = {
+            {AZ::Render::DisplayMapperOperationType::Reinhard, "Reinhard"},
+            {AZ::Render::DisplayMapperOperationType::GammaSRGB, "GammaSRGB"},
+            {AZ::Render::DisplayMapperOperationType::Passthrough, "Passthrough"},
+            {AZ::Render::DisplayMapperOperationType::AcesLut, "AcesLut"},
+            {AZ::Render::DisplayMapperOperationType::Aces, "Aces"}};
+
+        for (auto operationNamePair : m_operationNames)
+        {
+            m_operationActions[operationNamePair.first] = toneMappingMenu->addAction(operationNamePair.second, [operationNamePair]() {
+                MaterialCanvasViewportRequestBus::Broadcast(
+                    &MaterialCanvasViewportRequestBus::Events::SetDisplayMapperOperationType, operationNamePair.first);
+            });
+            m_operationActions[operationNamePair.first]->setCheckable(true);
+            m_operationActions[operationNamePair.first]->setChecked(
+                operationNamePair.first == viewportSettings->m_displayMapperOperationType);
+        }
+
+        toneMappingButton->setMenu(toneMappingMenu);
+        toneMappingButton->setText("Tone Mapping");
+        toneMappingButton->setIcon(QIcon(":/Icons/toneMapping.svg"));
+        toneMappingButton->setPopupMode(QToolButton::InstantPopup);
+        toneMappingButton->setVisible(true);
+        addWidget(toneMappingButton);
+
+        // Add lighting preset combo box
+        auto lightingPresetComboBox = new LightingPresetComboBox(this);
+        lightingPresetComboBox->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents);
+        lightingPresetComboBox->view()->setMinimumWidth(200);
+        addWidget(lightingPresetComboBox);
+
+        // Add model combo box
+        auto modelPresetComboBox = new ModelPresetComboBox(this);
+        modelPresetComboBox->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToContents);
+        modelPresetComboBox->view()->setMinimumWidth(200);
+        addWidget(modelPresetComboBox);
+
+        MaterialCanvasViewportNotificationBus::Handler::BusConnect();
+    }
+
+    MaterialCanvasToolBar::~MaterialCanvasToolBar()
+    {
+        MaterialCanvasViewportNotificationBus::Handler::BusDisconnect();
+    }
+
+    void MaterialCanvasToolBar::OnGridEnabledChanged(bool enable)
+    {
+        m_toggleGrid->setChecked(enable);
+    }
+
+    void MaterialCanvasToolBar::OnAlternateSkyboxEnabledChanged(bool enable)
+    {
+        m_toggleAlternateSkybox->setChecked(enable);
+    }
+
+    void MaterialCanvasToolBar::OnDisplayMapperOperationTypeChanged(AZ::Render::DisplayMapperOperationType operationType)
+    {
+        for (auto operationActionPair : m_operationActions)
+        {
+            operationActionPair.second->setChecked(operationActionPair.first == operationType);
+        }
+    }
+
+    void MaterialCanvasToolBar::OnShadowCatcherEnabledChanged(bool enable)
+    {
+        m_toggleShadowCatcher->setChecked(enable);
+    }
+
+} // namespace MaterialCanvas
+
+#include <Window/ToolBar/moc_MaterialCanvasToolBar.cpp>

+ 42 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/MaterialCanvasToolBar.h

@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <QAction>
+#include <QToolBar>
+#include <Viewport/MaterialCanvasViewportNotificationBus.h>
+#endif
+
+namespace MaterialCanvas
+{
+    class MaterialCanvasToolBar
+        : public QToolBar
+        , public MaterialCanvasViewportNotificationBus::Handler
+    {
+        Q_OBJECT
+    public:
+        MaterialCanvasToolBar(QWidget* parent = 0);
+        ~MaterialCanvasToolBar();
+
+    private:
+        // MaterialCanvasViewportNotificationBus::Handler overrides...
+        void OnShadowCatcherEnabledChanged([[maybe_unused]] bool enable) override;
+        void OnGridEnabledChanged([[maybe_unused]] bool enable) override;
+        void OnAlternateSkyboxEnabledChanged([[maybe_unused]] bool enable) override;
+        void OnDisplayMapperOperationTypeChanged(AZ::Render::DisplayMapperOperationType operationType) override;
+
+        QAction* m_toggleGrid = {};
+        QAction* m_toggleShadowCatcher = {};
+        QAction* m_toggleAlternateSkybox = {};
+
+        AZStd::unordered_map<AZ::Render::DisplayMapperOperationType, QString> m_operationNames;
+        AZStd::unordered_map<AZ::Render::DisplayMapperOperationType, QAction*> m_operationActions;
+    };
+} // namespace MaterialCanvas

+ 105 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/ModelPresetComboBox.cpp

@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/Feature/Utils/ModelPreset.h>
+#include <Viewport/MaterialCanvasViewportRequestBus.h>
+#include <Window/ToolBar/ModelPresetComboBox.h>
+
+namespace MaterialCanvas
+{
+    ModelPresetComboBox::ModelPresetComboBox(QWidget* parent)
+        : QComboBox(parent)
+    {
+        connect(this, static_cast<void(QComboBox::*)(const int)>(&QComboBox::currentIndexChanged), this, [this](int index) {
+            if (index >= 0 && index < m_presets.size())
+            {
+                MaterialCanvasViewportRequestBus::Broadcast(
+                    &MaterialCanvasViewportRequestBus::Events::SelectModelPreset, m_presets[index]);
+            }
+            });
+
+        Refresh();
+
+        MaterialCanvasViewportNotificationBus::Handler::BusConnect();
+    }
+
+    ModelPresetComboBox::~ModelPresetComboBox()
+    {
+        MaterialCanvasViewportNotificationBus::Handler::BusDisconnect();
+    }
+
+    void ModelPresetComboBox::Refresh()
+    {
+        clear();
+        setDuplicatesEnabled(true);
+
+        m_presets.clear();
+        MaterialCanvasViewportRequestBus::BroadcastResult(m_presets, &MaterialCanvasViewportRequestBus::Events::GetModelPresets);
+
+        AZStd::sort(m_presets.begin(), m_presets.end(), [](const auto& a, const auto& b) {
+            return a->m_displayName < b->m_displayName; });
+
+        blockSignals(true);
+        for (const auto& preset : m_presets)
+        {
+            addItem(preset->m_displayName.c_str());
+        }
+        blockSignals(false);
+
+        AZ::Render::ModelPresetPtr preset;
+        MaterialCanvasViewportRequestBus::BroadcastResult(preset, &MaterialCanvasViewportRequestBus::Events::GetModelPresetSelection);
+        OnModelPresetSelected(preset);
+    }
+
+    void ModelPresetComboBox::OnModelPresetSelected(AZ::Render::ModelPresetPtr preset)
+    {
+        auto presetItr = AZStd::find(m_presets.begin(), m_presets.end(), preset);
+        if (presetItr != m_presets.end())
+        {
+            setCurrentIndex(static_cast<int>(AZStd::distance(m_presets.begin(), presetItr)));
+        }
+    }
+
+    void ModelPresetComboBox::OnModelPresetAdded(AZ::Render::ModelPresetPtr preset)
+    {
+        if (!m_reloading)
+        {
+            Refresh();
+        }
+    }
+
+    void ModelPresetComboBox::OnModelPresetChanged(AZ::Render::ModelPresetPtr preset)
+    {
+        if (!m_reloading)
+        {
+            auto presetItr = AZStd::find(m_presets.begin(), m_presets.end(), preset);
+            if (presetItr != m_presets.end())
+            {
+                setItemText(static_cast<int>(AZStd::distance(m_presets.begin(), presetItr)), preset->m_displayName.c_str());
+            }
+            else
+            {
+                Refresh();
+            }
+        }
+    }
+
+    void ModelPresetComboBox::OnBeginReloadContent()
+    {
+        m_reloading = true;
+    }
+
+    void ModelPresetComboBox::OnEndReloadContent()
+    {
+        m_reloading = false;
+        Refresh();
+    }
+
+} // namespace MaterialCanvas
+
+#include <Window/ToolBar/moc_ModelPresetComboBox.cpp>

+ 38 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/Window/ToolBar/ModelPresetComboBox.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include <QComboBox>
+#include <Viewport/MaterialCanvasViewportNotificationBus.h>
+#endif
+
+namespace MaterialCanvas
+{
+    class ModelPresetComboBox
+        : public QComboBox
+        , public MaterialCanvasViewportNotificationBus::Handler
+    {
+        Q_OBJECT
+    public:
+        ModelPresetComboBox(QWidget* parent = 0);
+        ~ModelPresetComboBox();
+        void Refresh();
+    private:
+        // MaterialCanvasViewportNotificationBus::Handler overrides...
+        void OnModelPresetSelected(AZ::Render::ModelPresetPtr preset) override;
+        void OnModelPresetAdded(AZ::Render::ModelPresetPtr preset) override;
+        void OnModelPresetChanged(AZ::Render::ModelPresetPtr preset) override;
+        void OnBeginReloadContent() override;
+        void OnEndReloadContent() override;
+
+        bool m_reloading = false;
+        AZ::Render::ModelPresetPtrVector m_presets;
+    };
+} // namespace MaterialCanvas

+ 24 - 0
Gems/Atom/Tools/MaterialCanvas/Code/Source/main.cpp

@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <MaterialCanvasApplication.h>
+
+int main(int argc, char** argv)
+{
+    AzQtComponents::AzQtApplication::InitializeDpiScaling();
+
+    MaterialCanvas::MaterialCanvasApplication app(&argc, &argv);
+    if (app.LaunchLocalServer())
+    {
+        app.Start(AZ::ComponentApplication::Descriptor{});
+        app.exec();
+        app.Stop();
+    }
+
+    return 0;
+}

Неке датотеке нису приказане због велике количине промена