alexpete 4 anni fa
commit
69567068fb
100 ha cambiato i file con 19991 aggiunte e 0 eliminazioni
  1. 119 0
      .gitattributes
  2. 3 0
      .gitignore
  3. 2 0
      .lfsconfig
  4. 15 0
      AssetProcessorGamePlatformConfig.ini
  5. 13 0
      CMakeLists.txt
  6. 33 0
      Config.xml
  7. 114 0
      Config/Editor.xml
  8. 109 0
      Config/Game.xml
  9. 49 0
      Config/Game_RHI_Samples_Template.xml
  10. 26 0
      Config/ImageComparisonConfig.azasset
  11. 29 0
      Config/shader_global_build_options.json
  12. 12 0
      Gem/CMakeLists.txt
  13. 171 0
      Gem/Code/CMakeLists.txt
  14. 48 0
      Gem/Code/Lib/MaterialFunctors/StacksShaderCollectionFunctor.cpp
  15. 42 0
      Gem/Code/Lib/MaterialFunctors/StacksShaderCollectionFunctor.h
  16. 45 0
      Gem/Code/Lib/MaterialFunctors/StacksShaderInputFunctor.cpp
  17. 44 0
      Gem/Code/Lib/MaterialFunctors/StacksShaderInputFunctor.h
  18. 865 0
      Gem/Code/Source/AreaLightExampleComponent.cpp
  19. 229 0
      Gem/Code/Source/AreaLightExampleComponent.h
  20. 368 0
      Gem/Code/Source/AssetLoadTestComponent.cpp
  21. 107 0
      Gem/Code/Source/AssetLoadTestComponent.h
  22. 183 0
      Gem/Code/Source/AtomSampleViewerModule.cpp
  23. 18 0
      Gem/Code/Source/AtomSampleViewerOptions.h
  24. 27 0
      Gem/Code/Source/AtomSampleViewerRequestBus.h
  25. 233 0
      Gem/Code/Source/AtomSampleViewerSystemComponent.cpp
  26. 88 0
      Gem/Code/Source/AtomSampleViewerSystemComponent.h
  27. 113 0
      Gem/Code/Source/Automation/AssetStatusTracker.cpp
  28. 64 0
      Gem/Code/Source/Automation/AssetStatusTracker.h
  29. 50 0
      Gem/Code/Source/Automation/ImageComparisonConfig.cpp
  30. 50 0
      Gem/Code/Source/Automation/ImageComparisonConfig.h
  31. 1550 0
      Gem/Code/Source/Automation/ScriptManager.cpp
  32. 325 0
      Gem/Code/Source/Automation/ScriptManager.h
  33. 32 0
      Gem/Code/Source/Automation/ScriptRepeaterBus.h
  34. 1096 0
      Gem/Code/Source/Automation/ScriptReporter.cpp
  35. 254 0
      Gem/Code/Source/Automation/ScriptReporter.h
  36. 35 0
      Gem/Code/Source/Automation/ScriptRunnerBus.h
  37. 634 0
      Gem/Code/Source/Automation/ScriptableImGui.cpp
  38. 162 0
      Gem/Code/Source/Automation/ScriptableImGui.h
  39. 190 0
      Gem/Code/Source/AuxGeomExampleComponent.cpp
  40. 67 0
      Gem/Code/Source/AuxGeomExampleComponent.h
  41. 1052 0
      Gem/Code/Source/AuxGeomSharedDrawFunctions.cpp
  42. 44 0
      Gem/Code/Source/AuxGeomSharedDrawFunctions.h
  43. 656 0
      Gem/Code/Source/BistroBenchmarkComponent.cpp
  44. 192 0
      Gem/Code/Source/BistroBenchmarkComponent.h
  45. 472 0
      Gem/Code/Source/BloomExampleComponent.cpp
  46. 130 0
      Gem/Code/Source/BloomExampleComponent.h
  47. 159 0
      Gem/Code/Source/CheckerboardExampleComponent.cpp
  48. 84 0
      Gem/Code/Source/CheckerboardExampleComponent.h
  49. 258 0
      Gem/Code/Source/CommonSampleComponentBase.cpp
  50. 122 0
      Gem/Code/Source/CommonSampleComponentBase.h
  51. 523 0
      Gem/Code/Source/CullingAndLodExampleComponent.cpp
  52. 140 0
      Gem/Code/Source/CullingAndLodExampleComponent.h
  53. 116 0
      Gem/Code/Source/DecalContainer.cpp
  54. 56 0
      Gem/Code/Source/DecalContainer.h
  55. 144 0
      Gem/Code/Source/DecalExampleComponent.cpp
  56. 66 0
      Gem/Code/Source/DecalExampleComponent.h
  57. 389 0
      Gem/Code/Source/DepthOfFieldExampleComponent.cpp
  58. 105 0
      Gem/Code/Source/DepthOfFieldExampleComponent.h
  59. 718 0
      Gem/Code/Source/DiffuseGIExampleComponent.cpp
  60. 187 0
      Gem/Code/Source/DiffuseGIExampleComponent.h
  61. 288 0
      Gem/Code/Source/DynamicDrawExampleComponent.cpp
  62. 74 0
      Gem/Code/Source/DynamicDrawExampleComponent.h
  63. 308 0
      Gem/Code/Source/DynamicMaterialTestComponent.cpp
  64. 87 0
      Gem/Code/Source/DynamicMaterialTestComponent.h
  65. 129 0
      Gem/Code/Source/EntityLatticeTestComponent.cpp
  66. 72 0
      Gem/Code/Source/EntityLatticeTestComponent.h
  67. 45 0
      Gem/Code/Source/EntityUtilityFunctions.cpp
  68. 24 0
      Gem/Code/Source/EntityUtilityFunctions.h
  69. 29 0
      Gem/Code/Source/ExampleComponentBus.h
  70. 303 0
      Gem/Code/Source/ExposureExampleComponent.cpp
  71. 92 0
      Gem/Code/Source/ExposureExampleComponent.h
  72. 896 0
      Gem/Code/Source/LightCullingExampleComponent.cpp
  73. 258 0
      Gem/Code/Source/LightCullingExampleComponent.h
  74. 272 0
      Gem/Code/Source/MSAA_RPI_ExampleComponent.cpp
  75. 99 0
      Gem/Code/Source/MSAA_RPI_ExampleComponent.h
  76. 604 0
      Gem/Code/Source/MaterialHotReloadTestComponent.cpp
  77. 117 0
      Gem/Code/Source/MaterialHotReloadTestComponent.h
  78. 348 0
      Gem/Code/Source/MeshExampleComponent.cpp
  79. 99 0
      Gem/Code/Source/MeshExampleComponent.h
  80. 609 0
      Gem/Code/Source/MultiRenderPipelineExampleComponent.cpp
  81. 144 0
      Gem/Code/Source/MultiRenderPipelineExampleComponent.h
  82. 518 0
      Gem/Code/Source/MultiSceneExampleComponent.cpp
  83. 145 0
      Gem/Code/Source/MultiSceneExampleComponent.h
  84. 269 0
      Gem/Code/Source/MultiViewSingleSceneAuxGeomExampleComponent.cpp
  85. 63 0
      Gem/Code/Source/MultiViewSingleSceneAuxGeomExampleComponent.h
  86. 455 0
      Gem/Code/Source/ParallaxMappingExampleComponent.cpp
  87. 132 0
      Gem/Code/Source/ParallaxMappingExampleComponent.h
  88. 21 0
      Gem/Code/Source/Platform/Android/AtomSampleViewerOptions_Android.cpp
  89. 13 0
      Gem/Code/Source/Platform/Android/MultiThreadComponent_Traits_Platform.h
  90. 35 0
      Gem/Code/Source/Platform/Android/SampleComponentManager_Android.cpp
  91. 21 0
      Gem/Code/Source/Platform/Android/StreamingImageExampleComponent_Android.cpp
  92. 30 0
      Gem/Code/Source/Platform/Android/Utils_Android.cpp
  93. 22 0
      Gem/Code/Source/Platform/Android/additional_android_runtime_library.cmake
  94. 18 0
      Gem/Code/Source/Platform/Android/atomsampleviewer_android_files.cmake
  95. 21 0
      Gem/Code/Source/Platform/Linux/AtomSampleViewerOptions_Linux.cpp
  96. 13 0
      Gem/Code/Source/Platform/Linux/MultiThreadComponent_Traits_Platform.h
  97. 30 0
      Gem/Code/Source/Platform/Linux/SampleComponentManager_Linux.cpp
  98. 21 0
      Gem/Code/Source/Platform/Linux/StreamingImageExampleComponent_Linux.cpp
  99. 30 0
      Gem/Code/Source/Platform/Linux/Utils_Linux.cpp
  100. 10 0
      Gem/Code/Source/Platform/Linux/additional_linux_runtime_library.cmake

+ 119 - 0
.gitattributes

@@ -0,0 +1,119 @@
+
+#
+# Git LFS (see https://git-lfs.github.com/)
+#
+*.3ds filter=lfs diff=lfs merge=lfs -text
+*.DLL filter=lfs diff=lfs merge=lfs -text
+*.FBX filter=lfs diff=lfs merge=lfs -text
+*.PDB filter=lfs diff=lfs merge=lfs -text
+*.PNG filter=lfs diff=lfs merge=lfs -text
+*.TGA filter=lfs diff=lfs merge=lfs -text
+*.TIF filter=lfs diff=lfs merge=lfs -text
+*.a filter=lfs diff=lfs merge=lfs -text
+*.abc filter=lfs diff=lfs merge=lfs -text
+*.actor filter=lfs diff=lfs merge=lfs -text
+*.adb filter=lfs diff=lfs merge=lfs -text
+*.akd filter=lfs diff=lfs merge=lfs -text
+*.animevents filter=lfs diff=lfs merge=lfs -text
+*.animgraph filter=lfs diff=lfs merge=lfs -text
+*.anm filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bmp filter=lfs diff=lfs merge=lfs -text
+*.bnk filter=lfs diff=lfs merge=lfs -text
+*.bspace filter=lfs diff=lfs merge=lfs -text
+*.cab filter=lfs diff=lfs merge=lfs -text
+*.caf filter=lfs diff=lfs merge=lfs -text
+*.cal filter=lfs diff=lfs merge=lfs -text
+*.cdf filter=lfs diff=lfs merge=lfs -text
+*.cfi filter=lfs diff=lfs merge=lfs -text
+*.cfr filter=lfs diff=lfs merge=lfs -text
+*.cfx filter=lfs diff=lfs merge=lfs -text
+*.cgf filter=lfs diff=lfs merge=lfs -text
+*.chr filter=lfs diff=lfs merge=lfs -text
+*.chrparams filter=lfs diff=lfs merge=lfs -text
+*.cld filter=lfs diff=lfs merge=lfs -text
+*.comb filter=lfs diff=lfs merge=lfs -text
+*.cry filter=lfs diff=lfs merge=lfs -text
+*.ctc filter=lfs diff=lfs merge=lfs -text
+*.dat filter=lfs diff=lfs merge=lfs -text
+*.dba filter=lfs diff=lfs merge=lfs -text
+*.dds filter=lfs diff=lfs merge=lfs -text
+*.dds.[0-9] filter=lfs diff=lfs merge=lfs -text
+*.dlg filter=lfs diff=lfs merge=lfs -text
+*.dll filter=lfs diff=lfs merge=lfs -text
+*.dmg filter=lfs diff=lfs merge=lfs -text
+*.dylib filter=lfs diff=lfs merge=lfs -text
+*.emfxrecording filter=lfs diff=lfs merge=lfs -text
+*.ent filter=lfs diff=lfs merge=lfs -text
+*.exe filter=lfs diff=lfs merge=lfs -text
+*.exr filter=lfs diff=lfs merge=lfs -text
+*.fbx filter=lfs diff=lfs merge=lfs -text
+*.fdp filter=lfs diff=lfs merge=lfs -text
+*.font filter=lfs diff=lfs merge=lfs -text
+*.fontfamily filter=lfs diff=lfs merge=lfs -text
+*.fsq filter=lfs diff=lfs merge=lfs -text
+*.fxl filter=lfs diff=lfs merge=lfs -text
+*.gfx filter=lfs diff=lfs merge=lfs -text
+*.gif filter=lfs diff=lfs merge=lfs -text
+*.grd filter=lfs diff=lfs merge=lfs -text
+*.i_caf filter=lfs diff=lfs merge=lfs -text
+*.icns filter=lfs diff=lfs merge=lfs -text
+*.ico filter=lfs diff=lfs merge=lfs -text
+*.ik filter=lfs diff=lfs merge=lfs -text
+*.img filter=lfs diff=lfs merge=lfs -text
+*.jar filter=lfs diff=lfs merge=lfs -text
+*.jpeg filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*.lib filter=lfs diff=lfs merge=lfs -text
+*.lmg filter=lfs diff=lfs merge=lfs -text
+*.lut filter=lfs diff=lfs merge=lfs -text
+*.ly filter=lfs diff=lfs merge=lfs -text
+*.ma filter=lfs diff=lfs merge=lfs -text
+*.max filter=lfs diff=lfs merge=lfs -text
+*.mb filter=lfs diff=lfs merge=lfs -text
+*.mkv filter=lfs diff=lfs merge=lfs -text
+*.motion filter=lfs diff=lfs merge=lfs -text
+*.motionset filter=lfs diff=lfs merge=lfs -text
+*.mov filter=lfs diff=lfs merge=lfs -text
+*.mp2 filter=lfs diff=lfs merge=lfs -text
+*.mp3 filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text
+*.msi filter=lfs diff=lfs merge=lfs -text
+*.node filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.ocm filter=lfs diff=lfs merge=lfs -text
+*.ogg filter=lfs diff=lfs merge=lfs -text
+*.otf filter=lfs diff=lfs merge=lfs -text
+*.pak filter=lfs diff=lfs merge=lfs -text
+*.pcm filter=lfs diff=lfs merge=lfs -text
+*.pdb filter=lfs diff=lfs merge=lfs -text
+*.pdf filter=lfs diff=lfs merge=lfs -text
+*.pkat filter=lfs diff=lfs merge=lfs -text
+*.pkfx filter=lfs diff=lfs merge=lfs -text
+*.pkmm filter=lfs diff=lfs merge=lfs -text
+*.png filter=lfs diff=lfs merge=lfs -text
+*.ppm filter=lfs diff=lfs merge=lfs -text
+*.prototype filter=lfs diff=lfs merge=lfs -text
+*.psd filter=lfs diff=lfs merge=lfs -text
+*.pxheightfield filter=lfs diff=lfs merge=lfs -text
+*.pxmesh filter=lfs diff=lfs merge=lfs -text
+*.sbs filter=lfs diff=lfs merge=lfs -text
+*.sbsar filter=lfs diff=lfs merge=lfs -text
+*.sfo filter=lfs diff=lfs merge=lfs -text
+*.skin filter=lfs diff=lfs merge=lfs -text
+*.smtl filter=lfs diff=lfs merge=lfs -text
+*.so filter=lfs diff=lfs merge=lfs -text
+*.sprite filter=lfs diff=lfs merge=lfs -text
+*.sub filter=lfs diff=lfs merge=lfs -text
+*.tga filter=lfs diff=lfs merge=lfs -text
+*.tif filter=lfs diff=lfs merge=lfs -text
+*.tiff filter=lfs diff=lfs merge=lfs -text
+*.trp filter=lfs diff=lfs merge=lfs -text
+*.ttf filter=lfs diff=lfs merge=lfs -text
+*.usm filter=lfs diff=lfs merge=lfs -text
+*.veg filter=lfs diff=lfs merge=lfs -text
+*.wav filter=lfs diff=lfs merge=lfs -text
+*.webm filter=lfs diff=lfs merge=lfs -text
+*.wem filter=lfs diff=lfs merge=lfs -text
+*.wxs filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+_savebackup/
+.mayaSwatches/
+*.swatches

+ 2 - 0
.lfsconfig

@@ -0,0 +1,2 @@
+[lfs]
+url = https://d30gcrxpu6mewr.cloudfront.net/api/v1

+ 15 - 0
AssetProcessorGamePlatformConfig.ini

@@ -0,0 +1,15 @@
+[RC cgf]
+ignore=true
+
+[RC fbx]
+ignore=true
+
+; ppm files are used for screenshot comparison in automated test scripts
+[RC ppm]
+glob=*.ppm
+params=copy
+
+[ScanFolder AtomTestData]
+watch=@ENGINEROOT@/Gems/Atom/TestData
+recursive=1
+order=1000

+ 13 - 0
CMakeLists.txt

@@ -0,0 +1,13 @@
+#
+# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+# its licensors.
+#
+# For complete copyright and license terms please see the LICENSE at the root of this
+# distribution (the "License"). All use of this software is governed by the License,
+# or, if provided, by the license below or the license accompanying this file. Do not
+# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#
+
+add_subdirectory(Gem)
+add_subdirectory(Standalone)

+ 33 - 0
Config.xml

@@ -0,0 +1,33 @@
+<ObjectStream version="1">
+	<Class name="ComponentApplication::Descriptor" version="2" type="{70277A3E-2AF5-4309-9BBF-6161AFBDE792}">
+		<Class name="bool" field="useExistingAllocator" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="grabAllMemory" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecords" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="int" field="recordingMode" value="2" type="{72039442-EB38-4D42-A1AD-CB68F7E0EEF6}"/>
+		<Class name="AZ::u64" field="stackRecordLevels" value="5" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="autoIntegrityCheck" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="markUnallocatedMemory" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="doNotUsePools" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="enableScriptReflection" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="unsigned int" field="pageSize" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="poolPageSize" value="4096" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="blockAlignment" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="AZ::u64" field="blockSize" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedOS" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedDebug" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="enableDrilling" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="AZStd::vector" field="modules" type="{2BADE35A-6F1B-4698-B2BC-3373D010020C}">
+		</Class>
+        <Class name="AZStd::vector" field="modules" type="{2BADE35A-6F1B-4698-B2BC-3373D010020C}">
+        </Class>
+	</Class>
+	<Class name="AZ::Entity" version="2" type="{75651658-8663-478D-9090-2432DFCAFA44}">
+		<Class name="EntityId" field="Id" version="1" type="{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}">
+			<Class name="AZ::u64" field="id" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		</Class>
+		<Class name="AZStd::string" field="Name" value="SystemEntity" type="{EF8FF807-DDEE-4EB0-B678-4CA3A2C490A4}"/>
+		<Class name="bool" field="IsDependencyReady" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="AZStd::vector" field="Components" type="{2BADE35A-6F1B-4698-B2BC-3373D010020C}"/>
+	</Class>
+</ObjectStream>
+

+ 114 - 0
Config/Editor.xml

@@ -0,0 +1,114 @@
+<ObjectStream version="3">
+	<Class name="ComponentApplication::Descriptor" version="2" type="{70277A3E-2AF5-4309-9BBF-6161AFBDE792}">
+		<Class name="bool" field="useExistingAllocator" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="grabAllMemory" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecords" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecordsSaveNames" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecordsAttemptDecodeImmediately" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="int" field="recordingMode" value="2" type="{72039442-EB38-4D42-A1AD-CB68F7E0EEF6}"/>
+		<Class name="AZ::u64" field="stackRecordLevels" value="5" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="autoIntegrityCheck" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="markUnallocatedMemory" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="doNotUsePools" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="enableScriptReflection" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="unsigned int" field="pageSize" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="poolPageSize" value="4096" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="blockAlignment" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="AZ::u64" field="blockSize" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedOS" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedDebug" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="enableDrilling" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="useOverrunDetection" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="useMalloc" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="AZStd::vector" field="allocatorRemappings" type="{82897F6E-6389-5BEF-B427-761DB35AC1CC}"/>
+		<Class name="AZStd::vector" field="modules" type="{8E779F80-AEAA-565B-ABB1-DE10B18CF995}">
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Maestro.Editor.3b9a978ed6f742a1acb99f74379a342c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.TextureAtlas.5a149b6b3c964064bd4970f0e92f72e2.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.LmbrCentral.Editor.ff06785f7145416b9d46fde39098cb0c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.LyShine.Editor.0fefab3f13364722b2eab3b96ce2bf20.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.SceneProcessing.Editor.7c2578f634df4345aca98d671e39b8ab.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.EditorPythonBindings.Editor.b658359393884c4381c2fe2952b1472a.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.AtomToolsFramework.Editor.3e0ee0c27f204f5188146baac822d020.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI.Private.fb7f322c8bdb42228d9e155c954f98bd.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_Vulkan.Private.150d40d376124d98a388dfe890551c03.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_Vulkan.Builders.150d40d376124d98a388dfe890551c03.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_Metal.Private.5f27cdc951e64fe0be9d823dc7acbc28.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_Metal.Builders.5f27cdc951e64fe0be9d823dc7acbc28.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RPI.Builders.a218db9eb2114477b46600fea4441a6c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RPI.Editor.a218db9eb2114477b46600fea4441a6c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/> 
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Feature_Common.Builders.b58e5eed0901428ca78544b04dbd61bd.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Feature_Common.Editor.b58e5eed0901428ca78544b04dbd61bd.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Bootstrap.c7ff89ad6e8b4b45b2fadef2bcf12d6e.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Asset_Shader.Builders.d32452026dae4b7dba2ad89dbde9c48f.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_DX12.Private.e011969cf32442fdaac2443a960ab5ff.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_DX12.Builders.e011969cf32442fdaac2443a960ab5ff.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Component_DebugCamera.013d1b42ad314c929b292c143bcbf045.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.AtomLyIntegration_CommonFeatures.Editor.4e981f3b17394f5d84d674fff0f54f4f.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.ImageProcessingAtom.Editor.9d10b00be96045caa64c705e5772cb64.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_AtomBridge.Editor.b55b2738aa4a46c8b034fe98e6e5158b.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.AtomSampleViewerGem.2114099d7a8940c2813e93b8b417277e.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.AtomSampleViewerGem.Tools.2114099d7a8940c2813e93b8b417277e.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+		</Class>
+	</Class>
+	<Class name="AZ::Entity" version="2" type="{75651658-8663-478D-9090-2432DFCAFA44}">
+		<Class name="EntityId" field="Id" version="1" type="{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}">
+			<Class name="AZ::u64" field="id" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		</Class>
+		<Class name="AZStd::string" field="Name" value="SystemEntity" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+		<Class name="AZStd::vector" field="Components" type="{2BADE35A-6F1B-4698-B2BC-3373D010020C}"/>
+		<Class name="bool" field="IsDependencyReady" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="IsRuntimeActive" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+	</Class>
+</ObjectStream>

+ 109 - 0
Config/Game.xml

@@ -0,0 +1,109 @@
+<ObjectStream version="3">
+	<Class name="ComponentApplication::Descriptor" version="2" type="{70277A3E-2AF5-4309-9BBF-6161AFBDE792}">
+		<Class name="bool" field="useExistingAllocator" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="grabAllMemory" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecords" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecordsSaveNames" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecordsAttemptDecodeImmediately" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="int" field="recordingMode" value="2" type="{72039442-EB38-4D42-A1AD-CB68F7E0EEF6}"/>
+		<Class name="AZ::u64" field="stackRecordLevels" value="5" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="autoIntegrityCheck" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="markUnallocatedMemory" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="doNotUsePools" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="enableScriptReflection" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="unsigned int" field="pageSize" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="poolPageSize" value="4096" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="blockAlignment" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="AZ::u64" field="blockSize" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedOS" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedDebug" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="enableDrilling" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="useOverrunDetection" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="useMalloc" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="AZStd::vector" field="allocatorRemappings" type="{82897F6E-6389-5BEF-B427-761DB35AC1CC}"/>
+		<Class name="AZStd::vector" field="modules" type="{8E779F80-AEAA-565B-ABB1-DE10B18CF995}">
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Maestro.3b9a978ed6f742a1acb99f74379a342c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.TextureAtlas.5a149b6b3c964064bd4970f0e92f72e2.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.LmbrCentral.ff06785f7145416b9d46fde39098cb0c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.LyShine.0fefab3f13364722b2eab3b96ce2bf20.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI.Private.fb7f322c8bdb42228d9e155c954f98bd.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_Vulkan.Private.150d40d376124d98a388dfe890551c03.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_Metal.Private.5f27cdc951e64fe0be9d823dc7acbc28.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RPI.Private.a218db9eb2114477b46600fea4441a6c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Feature_Common.b58e5eed0901428ca78544b04dbd61bd.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Bootstrap.c7ff89ad6e8b4b45b2fadef2bcf12d6e.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_DX12.Private.e011969cf32442fdaac2443a960ab5ff.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Component_DebugCamera.013d1b42ad314c929b292c143bcbf045.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.AtomLyIntegration_CommonFeatures.4e981f3b17394f5d84d674fff0f54f4f.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_AtomBridge.b55b2738aa4a46c8b034fe98e6e5158b.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.AtomSampleViewerGem.2114099d7a8940c2813e93b8b417277e.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+		</Class>
+	</Class>
+	<Class name="AZ::Entity" version="2" type="{75651658-8663-478D-9090-2432DFCAFA44}">
+		<Class name="EntityId" field="Id" version="1" type="{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}">
+			<Class name="AZ::u64" field="id" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		</Class>
+		<Class name="AZStd::string" field="Name" value="SystemEntity" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+		<Class name="AZStd::vector" field="Components" type="{2BADE35A-6F1B-4698-B2BC-3373D010020C}"/>
+		<Class name="bool" field="IsDependencyReady" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="IsRuntimeActive" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+	</Class>
+	<Class name="ModuleEntity" type="{C5950488-35E0-4B55-B664-29A691A6482F}">
+		<Class name="AZ::Entity" field="BaseClass1" version="2" type="{75651658-8663-478D-9090-2432DFCAFA44}">
+			<Class name="EntityId" field="Id" version="1" type="{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}">
+				<Class name="AZ::u64" field="id" value="125826948175" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+			</Class>
+			<Class name="AZStd::string" field="Name" value="Gem.Atom_RHI.Private.fb7f322c8bdb42228d9e155c954f98bd.v0.1.0.dll" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+			<Class name="AZStd::vector" field="Components" type="{13D58FF9-1088-5C69-9A1F-C2A144B57B78}">
+				<Class name="FactoryRegistrationFinalizerSystemComponent" field="element" type="{03F8ABE7-C1A9-4B37-AA77-982A28CCA630}">
+					<Class name="AZ::Component" field="BaseClass1" type="{EDFCB2CF-F75D-43BE-B26B-F35821B29247}">
+						<Class name="AZ::u64" field="Id" value="3306328320536244301" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+					</Class>
+				</Class>
+				<Class name="FactoryManagerSystemComponent" field="element" type="{7C7AD991-9DD8-49D9-8C5F-6626937378E9}">
+					<Class name="AZ::Component" field="BaseClass1" type="{EDFCB2CF-F75D-43BE-B26B-F35821B29247}">
+						<Class name="AZ::u64" field="Id" value="15214617064832244664" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+					</Class>
+					<Class name="AZStd::vector" field="factoriesPriority" type="{99DAD0BC-740E-5E82-826B-8FC7968CC02C}">
+						<Class name="AZStd::string" field="element" value="dx12" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="element" value="vulkan" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+				</Class>
+			</Class>
+			<Class name="bool" field="IsDependencyReady" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+			<Class name="bool" field="IsRuntimeActive" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		</Class>
+		<Class name="AZ::Uuid" field="moduleClassId" value="{C34AA64E-0983-4D30-A33C-0D7C7676A20E}" type="{E152C105-A133-4D03-BBF8-3D4B2FBA3E2A}"/>
+	</Class>
+</ObjectStream>
+

+ 49 - 0
Config/Game_RHI_Samples_Template.xml

@@ -0,0 +1,49 @@
+<ObjectStream version="3">
+	<Class name="ComponentApplication::Descriptor" version="2" type="{70277A3E-2AF5-4309-9BBF-6161AFBDE792}">
+		<Class name="bool" field="useExistingAllocator" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="grabAllMemory" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecords" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecordsSaveNames" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="allocationRecordsAttemptDecodeImmediately" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="int" field="recordingMode" value="2" type="{72039442-EB38-4D42-A1AD-CB68F7E0EEF6}"/>
+		<Class name="AZ::u64" field="stackRecordLevels" value="5" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="autoIntegrityCheck" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="markUnallocatedMemory" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="doNotUsePools" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="enableScriptReflection" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="unsigned int" field="pageSize" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="poolPageSize" value="4096" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="unsigned int" field="blockAlignment" value="65536" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+		<Class name="AZ::u64" field="blockSize" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedOS" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="AZ::u64" field="reservedDebug" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		<Class name="bool" field="enableDrilling" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="AZStd::vector" field="modules" type="{8E779F80-AEAA-565B-ABB1-DE10B18CF995}">
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Asset_Shader.d32452026dae4b7dba2ad89dbde9c48f.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RHI_DX12.e011969cf32442fdaac2443a960ab5ff.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_RPI.Private.a218db9eb2114477b46600fea4441a6c.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.Atom_Component_DebugCamera.013d1b42ad314c929b292c143bcbf045.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+			<Class name="DynamicModuleDescriptor" field="element" type="{D2932FA3-9942-4FD2-A703-2E750F57C003}">
+				<Class name="AZStd::string" field="dynamicLibraryPath" value="Gem.AtomSampleViewerGem.2114099d7a8940c2813e93b8b417277e.v0.1.0" type="{189CC2ED-FDDE-5680-91D4-9F630A79187F}"/>
+			</Class>
+		</Class>
+	</Class>
+	<Class name="AZ::Entity" version="2" type="{75651658-8663-478D-9090-2432DFCAFA44}">
+		<Class name="EntityId" field="Id" version="1" type="{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}">
+			<Class name="AZ::u64" field="id" value="0" type="{D6597933-47CD-4FC8-B911-63F3E2B0993A}"/>
+		</Class>
+		<Class name="AZStd::string" field="Name" value="SystemEntity" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+		<Class name="AZStd::vector" field="Components" type="{0D23B755-6E8F-5C6C-B7C9-A352A55DC1DF}"/>
+		<Class name="bool" field="IsDependencyReady" value="false" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+		<Class name="bool" field="IsRuntimeActive" value="true" type="{A0CA880C-AFE4-43CB-926C-59AC48496112}"/>
+	</Class>
+</ObjectStream>
+

+ 26 - 0
Config/ImageComparisonConfig.azasset

@@ -0,0 +1,26 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "AtomSampleViewer::ImageComparisonConfig",
+    "ClassData": {
+        "toleranceLevels": [
+            // These values bust be arranged in order of increasing threshold value.
+            // If you need to add a new tolerance level in the middle, name it like "Level G.1" rather 
+            // than shifting the level names to avoid impacting test scripts that reference these names.
+            { "name": "Level A", "filterImperceptibleDiffs": false, "threshold": 0.0 },
+            { "name": "Level B", "filterImperceptibleDiffs": true,  "threshold": 0.0001 },
+            { "name": "Level C", "filterImperceptibleDiffs": true,  "threshold": 0.0005 },
+            { "name": "Level D", "filterImperceptibleDiffs": true,  "threshold": 0.001 },
+            { "name": "Level E", "filterImperceptibleDiffs": true,  "threshold": 0.003 },
+            { "name": "Level F", "filterImperceptibleDiffs": true,  "threshold": 0.005 },
+            { "name": "Level G", "filterImperceptibleDiffs": true,  "threshold": 0.01 },
+            { "name": "Level H", "filterImperceptibleDiffs": true,  "threshold": 0.025 },
+            { "name": "Level I", "filterImperceptibleDiffs": true,  "threshold": 0.05 },
+            { "name": "Level J", "filterImperceptibleDiffs": true,  "threshold": 0.06 },
+            { "name": "Level K", "filterImperceptibleDiffs": true,  "threshold": 0.07 },
+            { "name": "Level L", "filterImperceptibleDiffs": true,  "threshold": 0.08 },
+            { "name": "Level M", "filterImperceptibleDiffs": true,  "threshold": 0.09 },
+            { "name": "Level N", "filterImperceptibleDiffs": true,  "threshold": 0.1 }
+        ]
+    }
+}

+ 29 - 0
Config/shader_global_build_options.json

@@ -0,0 +1,29 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "GlobalBuildOptions",
+    "ClassData": {
+        "PreprocessorOptions" : {
+            "predefinedMacros": ["AZSL=17"],
+            "projectIncludePaths": [
+                "AtomSampleViewer",
+                // These include paths are already part of the automatic include folders list.
+                // By specifying them here, we are boosting the priority of these folders above all the other automatic include folders.
+                // (This is not necessary for the project, but just shown as a usage example.)
+                "Gems/Atom/RPI/Assets/ShaderLib",
+                "Gems/Atom/Feature/Common/Assets/ShaderLib"
+            ]
+        },
+        "ShaderCompilerArguments" : {
+            "AzslcWarningLevel" : 1,
+            "AzslcWarningAsError" : false,
+            "AzslcAdditionalFreeArguments" : "--strip-unused-srgs",
+            "DxcDisableWarnings" : false,
+            "DxcWarningAsError" : false,
+            "DxcDisableOptimizations" : false,
+            "DxcOptimizationLevel" : 3,
+            "DxcAdditionalFreeArguments" : "", 
+            "DefaultMatrixOrder" : "Row"
+        }
+    }
+}

+ 12 - 0
Gem/CMakeLists.txt

@@ -0,0 +1,12 @@
+#
+# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+# its licensors.
+#
+# For complete copyright and license terms please see the LICENSE at the root of this
+# distribution (the "License"). All use of this software is governed by the License,
+# or, if provided, by the license below or the license accompanying this file. Do not
+# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#
+
+add_subdirectory(Code)

+ 171 - 0
Gem/Code/CMakeLists.txt

@@ -0,0 +1,171 @@
+#
+# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+# its licensors.
+#
+# For complete copyright and license terms please see the LICENSE at the root of this
+# distribution (the "License"). All use of this software is governed by the License,
+# or, if provided, by the license below or the license accompanying this file. Do not
+# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#
+
+ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME})
+
+ly_add_target(
+    NAME AtomSampleViewer.Lib.Static STATIC
+    NAMESPACE Gem
+    FILES_CMAKE
+        atomsampleviewergem_lib_files.cmake
+    INCLUDE_DIRECTORIES
+        PUBLIC
+            Include
+                Lib
+    BUILD_DEPENDENCIES
+        PUBLIC
+            AZ::AzGameFramework
+            Gem::Atom_AtomBridge.Static
+            Gem::Atom_RPI.Public
+            Gem::Atom_Utils.Static
+)
+
+ly_add_target(
+    NAME AtomSampleViewer.Private.Static STATIC
+    NAMESPACE Gem
+    FILES_CMAKE
+        atomsampleviewergem_private_files.cmake
+        ${pal_dir}/atomsampleviewer_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake
+    INCLUDE_DIRECTORIES
+        PUBLIC
+            Include
+                Lib
+                Source
+                ${pal_dir}
+    BUILD_DEPENDENCIES
+        PUBLIC
+            AZ::AzGameFramework
+            Gem::Atom_AtomBridge.Static
+            Gem::Atom_Feature_Common.Static
+            Gem::Atom_Component_DebugCamera.Static
+            Gem::AtomSampleViewer.Lib.Static
+)
+
+ly_add_target(
+    NAME AtomSampleViewer ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
+    NAMESPACE Gem
+    OUTPUT_NAME Gem.AtomSampleViewerGem.2114099d7a8940c2813e93b8b417277e.v0.1.0
+    FILES_CMAKE
+        atomsampleviewergem_private_shared_files.cmake
+        ../../atomsampleviewer_asset_files.cmake
+    INCLUDE_DIRECTORIES
+        PUBLIC
+            Include
+    PLATFORM_INCLUDE_FILES
+        ${pal_dir}/additional_${PAL_PLATFORM_NAME_LOWERCASE}_runtime_library.cmake
+    BUILD_DEPENDENCIES
+        PRIVATE
+            AZ::AzGameFramework
+            Gem::Atom_AtomBridge.Static
+            Gem::AtomSampleViewer.Private.Static
+            Gem::ImGui.imguilib
+)
+
+if(PAL_TRAIT_BUILD_HOST_TOOLS)
+
+    ly_add_target(
+        NAME AtomSampleViewer.Tools.Static STATIC
+        NAMESPACE Gem
+        FILES_CMAKE
+            atomsampleviewergem_tools_files.cmake
+        INCLUDE_DIRECTORIES
+            PUBLIC
+                Include
+                    .
+                    Tools
+        BUILD_DEPENDENCIES
+            PRIVATE
+                AZ::AzGameFramework
+                Gem::Atom_AtomBridge.Static
+                Gem::AtomSampleViewer.Lib.Static
+            PUBLIC
+                Gem::Atom_RPI.Edit
+    )
+
+    ly_add_target(
+        NAME AtomSampleViewer.Tools MODULE
+        NAMESPACE Gem
+        OUTPUT_NAME Gem.AtomSampleViewerGem.Tools.2114099d7a8940c2813e93b8b417277e.v0.1.0
+        FILES_CMAKE
+            atomsampleviewergem_tools_shared_files.cmake
+        COMPILE_DEFINITIONS
+            PUBLIC
+        INCLUDE_DIRECTORIES
+            PRIVATE
+                .
+                Source
+                Source/Platform/${PAL_PLATFORM_NAME}
+            PUBLIC
+                Include
+        BUILD_DEPENDENCIES
+            PUBLIC
+                AZ::AzCore
+                Gem::AtomSampleViewer.Tools.Static
+    )
+
+endif()
+
+################################################################################
+# Gem dependencies
+################################################################################
+ly_add_project_dependencies(
+    PROJECT_NAME
+        AtomSampleViewer
+    TARGETS 
+        AtomSampleViewer.GameLauncher
+    DEPENDENCIES_FILES runtime_dependencies.cmake
+)
+
+if(PAL_TRAIT_BUILD_HOST_TOOLS)
+    ly_add_project_dependencies(
+        PROJECT_NAME
+            AtomSampleViewer
+        TARGETS
+            AssetBuilder
+            AssetProcessor
+            AssetProcessorBatch
+            Editor
+        DEPENDENCIES_FILES tool_dependencies.cmake
+    )
+endif()
+
+if(PAL_TRAIT_BUILD_SUPPORTS_SERVER)
+    ly_add_project_dependencies(
+        PROJECT_NAME
+            AtomSampleViewer
+        TARGETS
+            AtomSampleViewerServer
+        DEPENDENCIES_FILES runtime_dependencies.cmake
+    )
+endif()
+
+################################################################################
+# Tests
+################################################################################
+if(PAL_TRAIT_BUILD_SUPPORTS_TESTS)
+    ly_add_target(
+        NAME AtomSampleViewer.Tests MODULE
+        NAMESPACE Gem
+        FILES_CMAKE
+            atomsampleviewer_tests_files.cmake
+        INCLUDE_DIRECTORIES
+            PRIVATE
+                Tests
+        BUILD_DEPENDENCIES
+            PRIVATE
+                AZ::AzTest
+                Gem::AtomSampleViewer
+    )
+    ly_add_googletest(
+        NAME Gem::AtomSampleViewer.Tests
+        TARGET Gem::AtomSampleViewer.Tests
+    )
+endif()

+ 48 - 0
Gem/Code/Lib/MaterialFunctors/StacksShaderCollectionFunctor.cpp

@@ -0,0 +1,48 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MaterialFunctors/StacksShaderCollectionFunctor.h>
+#include <Atom/RPI.Public/Material/Material.h>
+#include <Atom/RPI.Reflect/Shader/ShaderOptionGroup.h>
+
+namespace AtomSampleViewer
+{
+    void StacksShaderCollectionFunctor::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<StacksShaderCollectionFunctor, AZ::RPI::MaterialFunctor>()
+                ->Version(1)
+                ->Field("m_stackCountProperty", &StacksShaderCollectionFunctor::m_stackCountProperty)
+                ->Field("m_highlightLastStackProperty", &StacksShaderCollectionFunctor::m_highlightLastStackProperty)
+                ->Field("m_highlightLastStackOption", &StacksShaderCollectionFunctor::m_highlightLastStackOption)
+                ;
+        }
+    }
+
+    void StacksShaderCollectionFunctor::Process(RuntimeContext& context)
+    {
+        using namespace AZ::RPI;
+
+        const uint32_t stackCount = context.GetMaterialPropertyValue<uint32_t>(m_stackCountProperty);
+        const bool highlightLastStack = context.GetMaterialPropertyValue<bool>(m_highlightLastStackProperty);
+
+        static const int AvailableStackCount = 4;
+        for (uint32_t i = 0; i < AvailableStackCount; ++i)
+        {
+            const bool isLastStack = (i == stackCount - 1);
+            const bool shouldHighlight = highlightLastStack && isLastStack;
+            context.SetShaderOptionValue(i, m_highlightLastStackOption, ShaderOptionValue{ shouldHighlight });
+            context.SetShaderEnabled(i, i < stackCount);
+        }
+    }
+} // namespace AtomSampleViewer

+ 42 - 0
Gem/Code/Lib/MaterialFunctors/StacksShaderCollectionFunctor.h

@@ -0,0 +1,42 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <Atom/RPI.Reflect/Material/MaterialFunctor.h>
+#include <Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h>
+
+namespace AtomSampleViewer
+{
+    //! This is an example of a custom hard-coded MaterialFunctor used to dynamically select shaders/variants/passes.
+    //! It is used by comprehensive.materialtype to enable/disable variants of the "stacks" shader.
+    class StacksShaderCollectionFunctor final
+        : public AZ::RPI::MaterialFunctor
+    {
+        friend class StacksShaderCollectionFunctorSourceData;
+    public:
+        AZ_RTTI(StacksShaderCollectionFunctor, "{4E51A7D5-7DF1-4402-8975-F6C9DFDEDC1E}", AZ::RPI::MaterialFunctor);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        void Process(RuntimeContext& context) override;
+
+    private:
+
+        // Indexes used to look up material property values at runtime
+        AZ::RPI::MaterialPropertyIndex m_stackCountProperty;
+        AZ::RPI::MaterialPropertyIndex m_highlightLastStackProperty;
+
+        // Indexes used to access ShaderOption values at runtime
+        AZ::RPI::ShaderOptionIndex m_highlightLastStackOption;
+    };
+} // namespace AtomSampleViewer

+ 45 - 0
Gem/Code/Lib/MaterialFunctors/StacksShaderInputFunctor.cpp

@@ -0,0 +1,45 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MaterialFunctors/StacksShaderInputFunctor.h>
+#include <Atom/RPI.Public/Material/Material.h>
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+#include <AzCore/Math/Matrix4x4.h>
+
+namespace AtomSampleViewer
+{
+    void StacksShaderInputFunctor::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<StacksShaderInputFunctor, AZ::RPI::MaterialFunctor>()
+                ->Version(1)
+                ->Field("m_azimuthDegreesIndex", &StacksShaderInputFunctor::m_azimuthDegreesIndex)
+                ->Field("m_elevationDegreesIndex", &StacksShaderInputFunctor::m_elevationDegreesIndex)
+                ->Field("m_lightDirectionIndex", &StacksShaderInputFunctor::m_lightDirectionIndex)
+                ;
+        }
+    }
+
+    void StacksShaderInputFunctor::Process(RuntimeContext& context)
+    {
+        float azimuthDegrees = context.GetMaterialPropertyValue<float>(m_azimuthDegreesIndex);
+        float elevationDegrees = context.GetMaterialPropertyValue<float>(m_elevationDegreesIndex);
+
+        AZ::Vector3 lightDir = AZ::Vector3(1,0,0) * AZ::Matrix4x4::CreateRotationZ(AZ::DegToRad(elevationDegrees)) * AZ::Matrix4x4::CreateRotationY(AZ::DegToRad(azimuthDegrees));
+
+        float floats[3];
+        lightDir.StoreToFloat3(floats);
+        context.GetShaderResourceGroup()->SetConstantRaw(m_lightDirectionIndex, floats, 3 * sizeof(float));
+    }
+
+} // namespace AtomSampleViewer

+ 44 - 0
Gem/Code/Lib/MaterialFunctors/StacksShaderInputFunctor.h

@@ -0,0 +1,44 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <Atom/RPI.Reflect/Material/MaterialFunctor.h>
+#include <Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h>
+
+namespace AtomSampleViewer
+{
+    //! This is an example of a custom hard-coded MaterialFunctor used to perform calculations on material 
+    //! property values to produce shader input values.
+    //! It is used by comprehensive.materialtype to transform angle values into a light direction vector.
+    class StacksShaderInputFunctor final
+        : public AZ::RPI::MaterialFunctor
+    {
+        friend class StacksShaderInputFunctorSourceData;
+    public:
+        AZ_RTTI(StacksShaderInputFunctor, "{7F607170-1BC2-4510-A252-8A665FC02052}", AZ::RPI::MaterialFunctor);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        void Process(RuntimeContext& context) override;
+
+    private:
+
+        // Indices used to look up material property values at runtime
+        AZ::RPI::MaterialPropertyIndex m_azimuthDegreesIndex;
+        AZ::RPI::MaterialPropertyIndex m_elevationDegreesIndex;
+
+        // Indices used to look up ShaderResourceGroup inputs at runtime
+        AZ::RHI::ShaderInputConstantIndex m_lightDirectionIndex;
+    };
+
+} // namespace AtomSampleViewer

+ 865 - 0
Gem/Code/Source/AreaLightExampleComponent.cpp

@@ -0,0 +1,865 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <AreaLightExampleComponent.h>
+#include <SampleComponentConfig.h>
+#include <Utils/Utils.h>
+#include <Automation/ScriptableImGui.h>
+
+#include <AzCore/Serialization/SerializeContext.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Public/AuxGeom/AuxGeomDraw.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+#include <Atom/RPI.Public/Image/StreamingImagePool.h>
+#include <Atom/RPI.Public/Shader/ShaderSystem.h>
+
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <imgui/imgui.h>
+#include <Atom/Feature/Material/MaterialAssignment.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    void AreaLightExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class <AreaLightExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    AZ::Quaternion AreaLightExampleComponent::Configuration::GetRotationQuaternion()
+    {
+        AZ::Quaternion rotation = AZ::Quaternion::CreateIdentity();
+        rotation.SetFromEulerRadians(AZ::Vector3(m_rotations[0] + AZ::Constants::Pi, m_rotations[1], m_rotations[2]));
+        return rotation;
+    }
+
+    AZ::Matrix3x3 AreaLightExampleComponent::Configuration::GetRotationMatrix()
+    {
+        return  AZ::Matrix3x3::CreateFromQuaternion(GetRotationQuaternion());
+    }
+
+    AreaLightExampleComponent::AreaLightExampleComponent()
+        : m_materialBrowser("@user@/AreaLightExampleComponent/material_browser.xml")
+        , m_modelBrowser("@user@/AreaLightExampleComponent/model_browser.xml")
+    {
+    }
+
+    void AreaLightExampleComponent::Activate()
+    {
+        // Get Feature processors
+        AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        m_meshFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::MeshFeatureProcessorInterface>();
+        m_pointLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PointLightFeatureProcessorInterface>();
+        m_diskLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DiskLightFeatureProcessorInterface>();
+        m_capsuleLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::CapsuleLightFeatureProcessorInterface>();
+        m_polygonLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PolygonLightFeatureProcessorInterface>();
+        m_quadLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::QuadLightFeatureProcessorInterface>();
+        m_skyBoxFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::SkyBoxFeatureProcessorInterface>();
+
+        m_auxGeom = AZ::RPI::AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(scene);
+
+        // Create background
+        m_skyBoxFeatureProcessor->SetSkyboxMode(AZ::Render::SkyBoxMode::Cubemap);
+        m_skyBoxFeatureProcessor->SetCubemap(Utils::GetSolidColorCubemap(0xFF202020));
+        m_skyBoxFeatureProcessor->Enable(true);
+
+        // Get material and set up material instances
+        AZ::RPI::AssetUtils::TraceLevel traceLevel = AZ::RPI::AssetUtils::TraceLevel::Assert;
+        static const char* defaultMaterialPath = "materials/presets/macbeth/00_illuminant.azmaterial";
+        auto materialAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath<AZ::RPI::MaterialAsset>(defaultMaterialPath, traceLevel);
+
+        InitializeMaterials(materialAsset);
+
+        // Prepare meshes and lights
+        auto modelAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>(m_config.m_modelAssetPath.c_str(), traceLevel);
+        m_meshHandles.resize(MaxVariants);
+        UpdateModels(modelAsset);
+
+        m_photometricValue.ConvertToPhotometricUnit(AZ::Render::PhotometricUnit::Lumen);
+        m_lightHandles.resize(MaxVariants);
+        UpdateLights();
+
+        // Enable camera
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+
+        // Sidebar
+        m_materialBrowser.SetFilter([this](const AZ::Data::AssetInfo& assetInfo)
+            {
+                return assetInfo.m_assetType == azrtti_typeid<AZ::RPI::MaterialAsset>() &&
+                    assetInfo.m_assetId.m_subId == 0; // no materials generated from models.
+
+            });
+        m_materialBrowser.Activate();
+        m_materialBrowserSettings.m_labels.m_root = "Materials";
+
+        m_modelBrowser.SetFilter([](const AZ::Data::AssetInfo& assetInfo)
+            {
+                return assetInfo.m_assetType == azrtti_typeid<AZ::RPI::ModelAsset>();
+            });
+        m_modelBrowser.Activate();
+        m_modelBrowserSettings.m_labels.m_root = "Models";
+
+        m_imguiSidebar.Activate();
+        m_imguiSidebar.SetHideSidebar(true);
+
+        // Connect to busses
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void AreaLightExampleComponent::Deactivate()
+    {
+        // Force validation off since it's a global flag.
+        AZ::RPI::ShaderSystemInterface::Get()->SetGlobalShaderOption(AZ::Name{ "o_area_light_validation" }, AZ::RPI::ShaderOptionValue{ false });
+
+        AZ::TickBus::Handler::BusDisconnect();
+
+        m_imguiSidebar.Deactivate();
+        m_modelBrowser.Deactivate();
+        m_materialBrowser.Deactivate();
+
+        ReleaseModels();
+        ReleaseLights();
+        m_materialInstances.clear();
+
+        m_skyBoxFeatureProcessor->SetSkyboxMode(AZ::Render::SkyBoxMode::None);
+        m_skyBoxFeatureProcessor->Enable(false);
+    }
+
+    void AreaLightExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        DrawUI();
+        DrawAuxGeom();
+    }
+
+    float AreaLightExampleComponent::GetPositionPercentage(uint32_t index)
+    {
+        return aznumeric_cast<float>(index) / aznumeric_cast<float>(m_config.m_count - 1);
+    }
+
+    AZ::Vector3 AreaLightExampleComponent::GetModelPosition(uint32_t index)
+    {
+        static float Spacing = 5.0f;
+
+        // Total width of n models is Spacing * n, so the start x position is half of that.
+        float startXPos = aznumeric_cast<float>(m_config.m_count - 1) * 0.5f * -Spacing;
+        float xPos = startXPos + aznumeric_cast<float>(index) * Spacing;
+
+        // y position pushes further away from the camera the more models there are to show.
+        float yPos = -2.0f + Spacing * m_config.m_count * 0.4f;
+
+        // z is slightly negative so the model's center is slightly below the camera's center
+        float zPos = -1.0f;
+        
+        return AZ::Vector3(xPos, yPos, zPos);
+    }
+
+    template<typename T>
+    T AreaLightExampleComponent::GetLerpValue(T values[2], uint32_t index, bool doLerp)
+    {
+        if (doLerp)
+        {
+            return AZ::Lerp(values[0], values[1], GetPositionPercentage(index));
+        }
+        return values[0];
+    }
+
+    AZ::Vector3 AreaLightExampleComponent::GetLightPosition(uint32_t index)
+    {
+        AZ::Vector3 position = GetModelPosition(index);
+        position.SetZ(position.GetZ() + m_config.m_lightDistance);
+        position += AZ::Vector3::CreateFromFloat3(m_config.m_positionOffset);
+        return position;
+    }
+
+    void AreaLightExampleComponent::InitializeMaterials(AZ::Data::Asset<AZ::RPI::MaterialAsset> materialAsset)
+    {
+        m_roughnessPropertyIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name("roughness.factor"));
+        m_metallicPropertyIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name("metallic.factor"));
+        m_multiScatteringEnabledIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name("specularF0.enableMultiScatterCompensation"));
+
+        m_materialInstances.resize(MaxVariants);
+        for (AZ::Data::Instance<AZ::RPI::Material>& material : m_materialInstances)
+        {
+            material = AZ::RPI::Material::Create(materialAsset);
+        }
+    }
+
+    void AreaLightExampleComponent::UpdateModels(AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset)
+    {
+        for (uint32_t i = 0; i < MaxVariants; ++i)
+        {
+            MeshHandle& meshHandle = m_meshHandles.at(i);
+
+            if (i < m_config.m_count)
+            {
+                if (!meshHandle.IsValid())
+                {
+                    meshHandle = m_meshFeatureProcessor->AcquireMesh(modelAsset, m_materialInstances.at(i));
+                }
+                else if (m_modelAsset.GetId() != modelAsset.GetId())
+                {
+                    // Valid mesh handle, but wrong asset. Release and reacquire.
+                    m_meshFeatureProcessor->ReleaseMesh(meshHandle);
+                    meshHandle = m_meshFeatureProcessor->AcquireMesh(modelAsset, m_materialInstances.at(i));
+                }
+
+                AZ::Transform transform = AZ::Transform::CreateIdentity();
+                transform.SetTranslation(GetModelPosition(i));
+
+                m_meshFeatureProcessor->SetTransform(meshHandle, transform);
+            }
+            else if(meshHandle.IsValid())
+            {
+                m_meshFeatureProcessor->ReleaseMesh(meshHandle);
+            }
+        }
+        m_modelAsset = modelAsset;
+    }
+
+    void AreaLightExampleComponent::UpdateMaterials()
+    {
+        bool allMaterialsCompiled = true;
+        for (uint32_t i = 0; i < m_config.m_count; ++i)
+        {
+            MaterialInstance& materialInstance = m_materialInstances.at(i);
+            if (m_roughnessPropertyIndex.IsValid())
+            {
+                float roughness = GetLerpValue(m_config.m_roughness, i, m_config.GetVaryRoughness());
+                materialInstance->SetPropertyValue(m_roughnessPropertyIndex, roughness);
+            }
+            if (m_metallicPropertyIndex.IsValid())
+            {
+                float metallic = GetLerpValue(m_config.m_metallic, i, m_config.GetVaryMetallic());
+                materialInstance->SetPropertyValue(m_metallicPropertyIndex, metallic);
+            }
+            if (m_multiScatteringEnabledIndex.IsValid())
+            {
+                materialInstance->SetPropertyValue(m_multiScatteringEnabledIndex, m_config.m_multiScattering);
+            }
+
+            allMaterialsCompiled = allMaterialsCompiled && materialInstance->Compile();
+        }
+        if (allMaterialsCompiled)
+        {
+            m_materialsNeedUpdate = false;
+        }
+    }
+
+    template <typename FeatureProcessorType, typename HandleType>
+    void AreaLightExampleComponent::UpdateLightForType(FeatureProcessorType featureProcessor, HandleType& handle, uint32_t index)
+    {
+        if (index < m_config.m_count)
+        {
+            if (!handle.IsValid())
+            {
+                handle = featureProcessor->AcquireLight();
+                featureProcessor->SetAttenuationRadius(handle, 100.0f);
+            }
+        }
+        else
+        {
+            featureProcessor->ReleaseLight(handle);
+        }
+    }
+
+    void AreaLightExampleComponent::UpdatePointLight(PointLightHandle& handle, uint32_t index, AZ::Vector3 position)
+    {
+        UpdateLightForType(m_pointLightFeatureProcessor, handle, index);
+
+        if (index < m_config.m_count)
+        {
+            m_photometricValue.SetEffectiveSolidAngle(AZ::Render::PhotometricValue::OmnidirectionalSteradians);
+            m_pointLightFeatureProcessor->SetRgbIntensity(handle, m_photometricValue.GetCombinedRgb<AZ::Render::PhotometricUnit::Candela>());
+
+            float radius = GetLerpValue(m_config.m_radius, index, m_config.GetVaryRadius());
+            m_pointLightFeatureProcessor->SetPosition(handle, position);
+            m_pointLightFeatureProcessor->SetBulbRadius(handle, radius);
+        }
+    }
+
+    void AreaLightExampleComponent::UpdateDiskLight(DiskLightHandle& handle, uint32_t index, AZ::Vector3 position)
+    {
+        UpdateLightForType(m_diskLightFeatureProcessor, handle, index);
+
+        if (index < m_config.m_count)
+        {
+            m_photometricValue.SetEffectiveSolidAngle(AZ::Render::PhotometricValue::DirectionalEffectiveSteradians);
+            m_diskLightFeatureProcessor->SetRgbIntensity(handle, m_photometricValue.GetCombinedRgb<AZ::Render::PhotometricUnit::Candela>());
+
+            m_diskLightFeatureProcessor->SetPosition(handle, position);
+
+            AZ::Matrix3x3 rotationMatrix = m_config.GetRotationMatrix();
+            m_diskLightFeatureProcessor->SetDirection(handle, rotationMatrix.GetBasisZ());
+
+            float radius = GetLerpValue(m_config.m_radius, index, m_config.GetVaryRadius());
+            m_diskLightFeatureProcessor->SetDiskRadius(handle, radius);
+
+            m_diskLightFeatureProcessor->SetLightEmitsBothDirections(handle, m_config.m_emitsBothDirections);
+        }
+    }
+
+    void AreaLightExampleComponent::UpdateCapsuleLight(CapsuleLightHandle& handle, uint32_t index, AZ::Vector3 position)
+    {
+        UpdateLightForType(m_capsuleLightFeatureProcessor, handle, index);
+
+        if (index < m_config.m_count)
+        {
+            m_photometricValue.SetEffectiveSolidAngle(AZ::Render::PhotometricValue::OmnidirectionalSteradians);
+            m_capsuleLightFeatureProcessor->SetRgbIntensity(handle, m_photometricValue.GetCombinedRgb<AZ::Render::PhotometricUnit::Candela>());
+
+            AZ::Matrix3x3 rotationMatrix = m_config.GetRotationMatrix();
+
+            AZ::Vector3 startPos = position - rotationMatrix.GetBasisZ() * m_config.m_capsuleHeight * 0.5f;
+            AZ::Vector3 endPos = position + rotationMatrix.GetBasisZ() * m_config.m_capsuleHeight * 0.5f;
+            m_capsuleLightFeatureProcessor->SetCapsuleLineSegment(handle, startPos, endPos);
+
+            float radius = GetLerpValue(m_config.m_radius, index, m_config.GetVaryRadius());
+            m_capsuleLightFeatureProcessor->SetCapsuleRadius(handle, radius);
+        }
+    }
+
+    void AreaLightExampleComponent::UpdateQuadLight(QuadLightHandle& handle, uint32_t index, AZ::Vector3 position)
+    {
+        UpdateLightForType(m_quadLightFeatureProcessor, handle, index);
+
+        if (index < m_config.m_count)
+        {
+            m_photometricValue.SetEffectiveSolidAngle(AZ::Render::PhotometricValue::DirectionalEffectiveSteradians);
+            m_photometricValue.SetArea(m_config.m_quadSize[0] * m_config.m_quadSize[1]);
+            m_quadLightFeatureProcessor->SetRgbIntensity(handle, m_photometricValue.GetCombinedRgb<AZ::Render::PhotometricUnit::Nit>());
+
+            m_quadLightFeatureProcessor->SetPosition(handle, position);
+            m_quadLightFeatureProcessor->SetOrientation(handle, m_config.GetRotationQuaternion());
+            m_quadLightFeatureProcessor->SetQuadDimensions(handle, m_config.m_quadSize[0], m_config.m_quadSize[1]);
+            m_quadLightFeatureProcessor->SetLightEmitsBothDirections(handle, m_config.m_emitsBothDirections);
+            m_quadLightFeatureProcessor->SetUseFastApproximation(handle, m_config.m_fastApproximation);
+        }
+    }
+
+    void AreaLightExampleComponent::UpdatePolygonLight(PolygonLightHandle& handle, uint32_t index, AZ::Vector3 position)
+    {
+        UpdateLightForType(m_polygonLightFeatureProcessor, handle, index);
+
+        if (index < m_config.m_count)
+        {
+            AZStd::vector<AZ::Vector3> points = GetPolygonVertices(m_config.m_polyStarCount, m_config.m_polyMinMaxRadius);
+
+            m_photometricValue.SetEffectiveSolidAngle(AZ::Render::PhotometricValue::DirectionalEffectiveSteradians);
+            m_photometricValue.SetArea(CalculatePolygonArea(points));
+            m_polygonLightFeatureProcessor->SetRgbIntensity(handle, m_photometricValue.GetCombinedRgb<AZ::Render::PhotometricUnit::Nit>());
+
+            m_polygonLightFeatureProcessor->SetPosition(handle, position);
+            m_polygonLightFeatureProcessor->SetLightEmitsBothDirections(handle, m_config.m_emitsBothDirections);
+
+            TransformVertices(points, m_config.GetRotationQuaternion(), position);
+
+            AZ::Vector3 direction = m_config.GetRotationQuaternion().TransformVector(AZ::Vector3::CreateAxisZ());
+            m_polygonLightFeatureProcessor->SetPolygonPoints(handle, points.data(), points.size(), direction);
+        }
+    }
+
+    void AreaLightExampleComponent::UpdateLights()
+    {
+
+        m_photometricValue.SetIntensity(m_config.m_intensity);
+        m_photometricValue.SetChroma(AZ::Color::CreateFromVector3(AZ::Vector3::CreateFromFloat3(m_config.m_color)));
+
+        for (uint32_t i = 0; i < MaxVariants; ++i)
+        {
+            LightHandle& lightHandle = m_lightHandles.at(i);
+            AZ::Vector3 lightPos = GetLightPosition(i);
+
+            switch (m_config.m_lightType)
+            {
+            case Point:
+                UpdatePointLight(lightHandle.m_point, i, lightPos);
+                break;
+            case Disk:
+                UpdateDiskLight(lightHandle.m_disk, i, lightPos);
+                break;
+            case Capsule:
+                UpdateCapsuleLight(lightHandle.m_capsule, i, lightPos);
+                break;
+            case Quad:
+                UpdateQuadLight(lightHandle.m_quad, i, lightPos);
+                break;
+            case Polygon:
+                UpdatePolygonLight(lightHandle.m_polygon, i, lightPos);
+                break;
+            }
+        }
+    }
+
+    void AreaLightExampleComponent::TransformVertices(AZStd::vector<AZ::Vector3>& vertices, const AZ::Quaternion& orientation, const AZ::Vector3& translation)
+    {
+        for (AZ::Vector3& vertex : vertices)
+        {
+            vertex = orientation.TransformVector(vertex);
+            vertex += translation;
+        }
+    }
+
+    AZ::Vector3 AreaLightExampleComponent::GetCirclePoint(float n, float count)
+    {
+        // Calculate angle for this point in the star
+        float ratio = 1.0f - (n / count);
+        float angle = ratio * AZ::Constants::TwoPi;
+
+        // Get normalized x, y coordinates for point
+        float sin = 0.0f;
+        float cos = 0.0f;
+        AZ::SinCos(angle, sin, cos);
+
+        return AZ::Vector3(sin, cos, 0.0f);
+    }
+
+    AZStd::vector<AZ::Vector3> AreaLightExampleComponent::GetPolygonVertices(uint32_t pointCount, float minMaxRadius[2])
+    {
+        uint32_t vertexCount = pointCount * 2; // For each of the stars points, there's a vertex bewteen the points.
+        AZStd::vector<AZ::Vector3> points;
+        points.reserve(vertexCount);
+
+        for (uint32_t i = 0; i < vertexCount; ++i)
+        {
+            // Get a point on the circle and scale it by the min or max radius for every other point.
+            AZ::Vector3 point = GetCirclePoint(i, vertexCount) * minMaxRadius[i % 2];
+            points.push_back(point);
+        }
+        return points;
+    }
+
+    AZStd::vector<AZ::Vector3> AreaLightExampleComponent::GetPolygonTriangles(uint32_t pointCount, float minMaxRadius[2])
+    {
+        AZStd::vector<AZ::Vector3> tris;
+        tris.reserve(pointCount * 6); // 2 triangles with 3 vertices for each star point.
+        uint32_t vertexCount = pointCount * 2; // For each of the stars points, there's a vertex bewteen the points.
+
+        for (uint32_t i = 0; i < vertexCount; ++i)
+        {
+            uint32_t nextI = (i + 1) % vertexCount;
+
+            AZ::Vector3 p0 = GetCirclePoint(i, vertexCount) * minMaxRadius[i % 2];
+            AZ::Vector3 p1 = GetCirclePoint(nextI, vertexCount) * minMaxRadius[nextI % 2];
+
+            tris.push_back(p0);
+            tris.push_back(p1);
+            tris.push_back(AZ::Vector3::CreateZero());
+        }
+        return tris;
+    }
+
+    void AreaLightExampleComponent::ReleaseModels()
+    {
+        for (MeshHandle& meshHandle : m_meshHandles)
+        {
+            m_meshFeatureProcessor->ReleaseMesh(meshHandle);
+        }
+    }
+
+    void AreaLightExampleComponent::ReleaseLights()
+    {
+        for (LightHandle& lightHandle : m_lightHandles)
+        {
+            switch (m_config.m_lightType)
+            {
+            case Point:
+                m_pointLightFeatureProcessor->ReleaseLight(lightHandle.m_point);
+                break;
+            case Disk:
+                m_diskLightFeatureProcessor->ReleaseLight(lightHandle.m_disk);
+                break;
+            case Capsule:
+                m_capsuleLightFeatureProcessor->ReleaseLight(lightHandle.m_capsule);
+                break;
+            case Quad:
+                m_quadLightFeatureProcessor->ReleaseLight(lightHandle.m_quad);
+                break;
+            case Polygon:
+                m_polygonLightFeatureProcessor->ReleaseLight(lightHandle.m_polygon);
+                break;
+            }
+        }
+    }
+
+    void AreaLightExampleComponent::DrawUI()
+    {
+        ScriptableImGui::Begin("AreaLightSample", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar);
+
+        bool modelsNeedUpdate = false;
+        bool lightsNeedUpdate = false;
+
+        ImGui::Text("Area Light Example");
+        ImGui::Separator();
+        ImGui::Text("Mesh Settings");
+
+        int count = m_config.m_count;
+        if (ScriptableImGui::SliderInt("Count", &count, 1, MaxVariants))
+        {
+            m_config.m_count = aznumeric_cast<uint32_t>(count);
+            modelsNeedUpdate = true;
+            lightsNeedUpdate = true;
+        }
+
+        if (m_roughnessPropertyIndex.IsValid())
+        {
+            if (m_config.m_count > 1)
+            {
+                if (ScriptableImGui::Checkbox("Vary Roughness Across Models", &m_config.m_varyRoughness))
+                {
+                    m_materialsNeedUpdate = true;
+                }
+            }
+            if (m_config.GetVaryRoughness())
+            {
+                if (ScriptableImGui::SliderFloat2("Min Max Roughness", m_config.m_roughness, 0.0f, 1.0f))
+                {
+                    m_materialsNeedUpdate = true;
+                }
+            }
+            else if (ScriptableImGui::SliderFloat("Roughness", &m_config.m_roughness[0], 0.0f, 1.0f))
+            {
+                m_materialsNeedUpdate = true;
+            }
+        }
+
+        if (m_metallicPropertyIndex.IsValid())
+        {
+            if (m_config.m_count > 1)
+            {
+                if (ScriptableImGui::Checkbox("Vary Metallic Across Models", &m_config.m_varyMetallic))
+                {
+                    m_materialsNeedUpdate = true;
+                }
+            }
+            if (m_config.GetVaryMetallic())
+            {
+                if (ScriptableImGui::SliderFloat2("Min Max Metallic", m_config.m_metallic, 0.0f, 1.0f))
+                {
+                    m_materialsNeedUpdate = true;
+                }
+            }
+            else if (ScriptableImGui::SliderFloat("Metallic", &m_config.m_metallic[0], 0.0f, 1.0f))
+            {
+                m_materialsNeedUpdate = true;
+            }
+        }
+
+        ImGui::Separator();
+        ImGui::Text("Light Settings");
+
+        if (ScriptableImGui::Checkbox("Validate", &m_config.m_validation))
+        {
+            AZ::RPI::ShaderSystemInterface::Get()->SetGlobalShaderOption(AZ::Name{ "o_area_light_validation" }, AZ::RPI::ShaderOptionValue{ m_config.m_validation });
+        }
+
+        AZStd::vector<AZStd::string> lightTypeNames
+        {
+            "Point",
+            "Disk",
+            "Capsule",
+            "Quad",
+            "Polygon",
+        };
+
+        if (ScriptableImGui::BeginCombo("LightType", lightTypeNames.at(m_config.m_lightType).c_str()))
+        {
+            for (uint32_t i = 0; i < lightTypeNames.size(); ++i)
+            {
+                AZStd::string& name = lightTypeNames.at(i);
+                if (ScriptableImGui::Selectable(name.c_str(), m_config.m_lightType == LightType(i)))
+                {
+                    ReleaseLights();
+                    m_config.m_lightType = LightType(i);
+                    lightsNeedUpdate = true;
+                }
+            }
+            ScriptableImGui::EndCombo();
+        }
+
+        if (ScriptableImGui::SliderFloat("Lumens", &m_config.m_intensity, 0.0f, 1000.0f, "%.3f", 2.0f))
+        {
+            lightsNeedUpdate = true;
+        }
+        if (ScriptableImGui::ColorEdit3("Color", m_config.m_color, ImGuiColorEditFlags_Float))
+        {
+            lightsNeedUpdate = true;
+        }
+        if (ScriptableImGui::SliderFloat3("Position Offset", m_config.m_positionOffset, -3.0f, 3.0f))
+        {
+            lightsNeedUpdate = true;
+        }
+
+        if (m_config.m_lightType == Disk || m_config.m_lightType == Capsule || m_config.m_lightType == Quad || m_config.m_lightType == Polygon)
+        {
+            if (ScriptableImGui::SliderAngle("X rotation", &m_config.m_rotations[0]))
+            {
+                lightsNeedUpdate = true;
+            }
+            if (ScriptableImGui::SliderAngle("Y rotation", &m_config.m_rotations[1]))
+            {
+                lightsNeedUpdate = true;
+            }
+            if (m_config.m_lightType == Quad || m_config.m_lightType == Polygon) // Disk and Capsule are circular around z axis, so this only affects Quad and Polygon.
+            {
+                if (ScriptableImGui::SliderAngle("Z rotation", &m_config.m_rotations[2]))
+                {
+                    lightsNeedUpdate = true;
+                }
+            }
+        }
+
+        // Radius
+        if (m_config.m_lightType == Point || m_config.m_lightType == Disk || m_config.m_lightType == Capsule)
+        {
+            if (m_config.m_count > 1)
+            {
+                if (ScriptableImGui::Checkbox("Vary Radius Across Lights", &m_config.m_varyRadius))
+                {
+                    lightsNeedUpdate = true;
+                }
+            }
+            if (m_config.GetVaryRadius())
+            {
+                if (ScriptableImGui::SliderFloat2("Min/Max Radius", m_config.m_radius, 0.0f, 1.0f))
+                {
+                    lightsNeedUpdate = true;
+                }
+            }
+            else if (ScriptableImGui::SliderFloat("Radius", &m_config.m_radius[0], 0.0f, 2.0f))
+            {
+                lightsNeedUpdate = true;
+            }
+        }
+
+        // Capsule Height
+        if (m_config.m_lightType == Capsule)
+        {
+            if (ScriptableImGui::SliderFloat("Capsule Height", &m_config.m_capsuleHeight, 0.0f, 10.0f))
+            {
+                lightsNeedUpdate = true;
+            }
+        }
+
+        if (m_config.m_lightType == Quad)
+        {
+            if (ScriptableImGui::SliderFloat2("Quad Dimensions", m_config.m_quadSize, 0.0f, 10.0f))
+            {
+                lightsNeedUpdate = true;
+            }
+            if (ScriptableImGui::Checkbox("Use Fast Approximation", &m_config.m_fastApproximation))
+            {
+                lightsNeedUpdate = true;
+            }
+        }
+
+        if (m_config.m_lightType == Polygon)
+        {
+            if (ScriptableImGui::SliderInt("Star Points", &m_config.m_polyStarCount, 2, 32))
+            {
+                lightsNeedUpdate = true;
+            }
+            if (ScriptableImGui::SliderFloat2("Star Min-Max Radius", m_config.m_polyMinMaxRadius, 0.0f, 4.0))
+            {
+                lightsNeedUpdate = true;
+            }
+        }
+
+        if (m_config.m_lightType == Disk || m_config.m_lightType == Quad || m_config.m_lightType == Polygon)
+        {
+            if (ScriptableImGui::Checkbox("Emit Both Directions", &m_config.m_emitsBothDirections))
+            {
+                lightsNeedUpdate = true;
+            }
+        }
+
+        if (ScriptableImGui::Checkbox("Multiscattering", &m_config.m_multiScattering))
+        {
+            m_materialsNeedUpdate = true;
+        }
+
+        ScriptableImGui::End();
+
+        if (m_imguiSidebar.Begin())
+        {
+            if (m_modelBrowser.Tick(m_modelBrowserSettings))
+            {
+                AZ::Data::AssetId selectedModelAssetId = m_modelBrowser.GetSelectedAssetId();
+                if (selectedModelAssetId.IsValid())
+                {
+                    AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset;
+                    modelAsset.Create(selectedModelAssetId);
+                    UpdateModels(modelAsset);
+                    modelsNeedUpdate = false;
+                }
+            }
+
+            ImGui::Spacing();
+
+            if (m_materialBrowser.Tick(m_materialBrowserSettings))
+            {
+                AZ::Data::AssetId selectedMaterialAssetId = m_materialBrowser.GetSelectedAssetId();
+                if (selectedMaterialAssetId.IsValid())
+                {
+                    AZ::Data::Asset<AZ::RPI::MaterialAsset> materialAsset = AZ::Data::AssetManager::Instance().GetAsset<AZ::RPI::MaterialAsset>(
+                        selectedMaterialAssetId, AZ::Data::AssetLoadBehavior::PreLoad);
+                    materialAsset.BlockUntilLoadComplete();
+
+                    if (materialAsset.IsReady())
+                    {
+                        InitializeMaterials(materialAsset);
+                        ReleaseModels();
+                        modelsNeedUpdate = true;
+                    }
+                }
+            }
+
+            m_imguiSidebar.End();
+        }
+
+        if (modelsNeedUpdate)
+        {
+            UpdateModels(m_modelAsset);
+        }
+        if (m_materialsNeedUpdate || modelsNeedUpdate)
+        {
+            UpdateMaterials();
+        }
+        if (lightsNeedUpdate)
+        {
+            UpdateLights();
+        }
+
+    }
+
+    void AreaLightExampleComponent::DrawAuxGeom()
+    {
+        // Lights need to add support for rendering their emissive surfaces to the regular forward pass which will replace the below.
+
+        // Draw AuxGeom for the lights themselves
+
+        AZ::Matrix3x3 rotationMatrix = m_config.GetRotationMatrix();
+
+        for (uint32_t i = 0; i < m_config.m_count; ++i)
+        {
+            float radius = GetLerpValue(m_config.m_radius, i, m_config.GetVaryRadius());
+            AZ::Vector3 lightPos = GetLightPosition(i);
+
+            float area = 0.0f;
+            switch (m_config.m_lightType)
+            {
+            case Point:
+                area = 4.0f * AZ::Constants::Pi * radius * radius;
+                break;
+            case Disk:
+                area = AZ::Constants::Pi * radius * radius;
+                break;
+            case Capsule:
+            {
+                float cylinderArea = 2.0f * AZ::Constants::Pi * m_config.m_capsuleHeight * radius;
+                float capArea = 4.0f * AZ::Constants::Pi * radius * radius;
+                area = cylinderArea + capArea;
+                break;
+            }
+            case Quad:
+                area = m_config.m_quadSize[0] * m_config.m_quadSize[1];
+                break;
+            case Polygon:
+                area = CalculatePolygonArea(GetPolygonVertices(m_config.m_polyStarCount, m_config.m_polyMinMaxRadius));
+                break;
+            }
+
+
+            float luxIntensity = m_config.m_intensity / area;
+            float nitsIntensity = luxIntensity / AZ::Constants::Pi;
+
+            // The aux geom pass happens after display mapper pass, so do basic gamma correction so the surface of the light's brightness is closer to everything else.
+            nitsIntensity = pow(nitsIntensity, 1.0f / 2.2f);
+
+            AZ::Color nitsColor = AZ::Color(nitsIntensity * m_config.m_color[0], nitsIntensity * m_config.m_color[1], nitsIntensity * m_config.m_color[2], 1.0f);
+
+            AZ::RPI::AuxGeomDraw::DrawStyle drawStyle = AZ::RPI::AuxGeomDraw::DrawStyle::Solid;
+
+            switch (m_config.m_lightType)
+            {
+            case Point:
+                m_auxGeom->DrawSphere(lightPos, radius, nitsColor, drawStyle);
+                break;
+            case Disk:
+                m_auxGeom->DrawDisk(lightPos, rotationMatrix.GetBasisZ(), radius, nitsColor, drawStyle);
+                break;
+            case Capsule:
+            {
+                m_auxGeom->DrawCylinder(lightPos, rotationMatrix.GetBasisZ(), radius, m_config.m_capsuleHeight, nitsColor, drawStyle);
+
+                // Draw cylinder caps as spheres
+                AZ::Vector3 startPos = lightPos - rotationMatrix.GetBasisZ() * m_config.m_capsuleHeight * 0.5f;
+                AZ::Vector3 endPos = lightPos + rotationMatrix.GetBasisZ() * m_config.m_capsuleHeight * 0.5f;
+                m_auxGeom->DrawSphere(startPos, radius, nitsColor, drawStyle);
+                m_auxGeom->DrawSphere(endPos, radius, nitsColor, drawStyle);
+                break;
+            }
+            case Quad:
+            {
+                AZ::Transform transform = AZ::Transform::CreateIdentity();
+                transform.SetRotation(AZ::ConvertEulerRadiansToQuaternion(AZ::Vector3(m_config.m_rotations[0], -m_config.m_rotations[1], m_config.m_rotations[2])));
+                transform.SetTranslation(lightPos);
+                transform *= AZ::Transform::CreateFromQuaternion(AZ::ConvertEulerRadiansToQuaternion(AZ::Vector3(AZ::Constants::Pi * 0.5f, 0.0f, 0.0f)));
+                m_auxGeom->DrawQuad(m_config.m_quadSize[0], m_config.m_quadSize[1], transform, nitsColor, drawStyle);
+                break;
+            }
+            case Polygon:
+            {
+                // Sadly DrawTriangles() only supports 8 bit color, so nitsColor must be capped at 1.0f.
+                nitsIntensity = AZ::GetMin(1.0f, nitsIntensity);
+                nitsColor = AZ::Color(nitsIntensity * m_config.m_color[0], nitsIntensity * m_config.m_color[1], nitsIntensity * m_config.m_color[2], 1.0f);
+
+                AZStd::vector<AZ::Vector3> tris = GetPolygonTriangles(m_config.m_polyStarCount, m_config.m_polyMinMaxRadius);
+                TransformVertices(tris, m_config.GetRotationQuaternion(), lightPos);
+
+                AZ::RPI::AuxGeomDraw::AuxGeomDynamicDrawArguments args;
+                args.m_colorCount = 1;
+                args.m_colors = &nitsColor;
+                args.m_vertCount = tris.size();
+                args.m_verts = tris.data();
+                m_auxGeom->DrawTriangles(args);
+                break;
+            }
+            }
+        }
+   }
+
+    float AreaLightExampleComponent::CalculatePolygonArea(const AZStd::vector<AZ::Vector3>& vertices)
+    {
+        // See https://en.wikipedia.org/wiki/Shoelace_formula
+        float twiceArea = 0.0f;
+        for (size_t i = 0; i < vertices.size(); ++i)
+        {
+            size_t j = (i + 1) % vertices.size();
+            twiceArea += vertices.at(i).GetX() * vertices.at(j).GetY();
+            twiceArea -= vertices.at(i).GetY() * vertices.at(j).GetX();
+        }
+        return AZ::GetAbs(twiceArea * 0.5f);
+    }
+
+}

+ 229 - 0
Gem/Code/Source/AreaLightExampleComponent.h

@@ -0,0 +1,229 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <AzCore/Component/TickBus.h>
+
+#include <Atom/RPI.Public/AuxGeom/AuxGeomFeatureProcessorInterface.h>
+
+#include <Atom/Feature/Mesh/MeshFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/CapsuleLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/DiskLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/PointLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/PolygonLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/QuadLightFeatureProcessorInterface.h>
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+
+#include <Utils/ImGuiAssetBrowser.h>
+#include <Utils/ImGuiSidebar.h>
+
+namespace AtomSampleViewer
+{
+    // This component renders a model with pbr material using checkerboard render pipeline.
+    class AreaLightExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(AreaLightExampleComponent, "{1CFEDA71-9459-44CE-A88B-F0CAE9192819}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        AreaLightExampleComponent();
+        ~AreaLightExampleComponent() override = default;
+
+        // AZ::Component overrides...
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        static const uint32_t MaxVariants = 10;
+
+        using MeshHandle = AZ::Render::MeshFeatureProcessorInterface::MeshHandle;
+        using PointLightHandle = AZ::Render::PointLightFeatureProcessorInterface::LightHandle;
+        using DiskLightHandle = AZ::Render::DiskLightFeatureProcessorInterface::LightHandle;
+        using CapsuleLightHandle = AZ::Render::CapsuleLightFeatureProcessorInterface::LightHandle;
+        using QuadLightHandle = AZ::Render::QuadLightFeatureProcessorInterface::LightHandle;
+        using PolygonLightHandle = AZ::Render::PolygonLightFeatureProcessorInterface::LightHandle;
+        using MaterialInstance = AZ::Data::Instance<AZ::RPI::Material>;
+
+        enum LightType
+        {
+            Point,
+            Disk,
+            Capsule,
+            Quad,
+            Polygon,
+        };
+
+        union LightHandle
+        {
+            LightHandle() { m_point.Reset(); };
+            PointLightHandle m_point;
+            DiskLightHandle m_disk;
+            CapsuleLightHandle m_capsule;
+            QuadLightHandle m_quad;
+            PolygonLightHandle m_polygon;
+        };
+
+        // Various data the user can alter in ImGui stored in ImGui friendly types.
+        struct Configuration
+        {
+            LightType m_lightType = Point;
+            AZStd::string m_modelAssetPath = "models/sphere.azmodel";
+            uint32_t m_count = 1;
+
+            float m_intensity = 30.0f;
+            float m_color[3] = { 1.0f, 1.0f, 1.0f };
+
+            float m_lightDistance = 3.0f;
+            float m_positionOffset[3] = { 0.0f, 0.0f, 0.0f };
+
+            float m_rotations[3] = { 0.0f, 0.0f, 0.0f };
+            
+            bool m_varyRadius = false;
+            float m_radius[2] = { 0.1f, 1.0f };
+
+            bool m_varyRoughness = false;
+            float m_roughness[2] = { 1.0f, 0.0f };
+
+            bool m_varyMetallic = false;
+            float m_metallic[2] = { 0.0f, 1.0f };
+
+            float m_capsuleHeight = 2.0f;
+
+            float m_quadSize[2] = { 1.0f, 1.0f };
+
+            int32_t m_polyStarCount = 5;
+            float m_polyMinMaxRadius[2] = { 0.25f, 0.5f };
+
+            bool m_emitsBothDirections = false;
+            bool m_validation = false;
+            bool m_fastApproximation = false;
+            bool m_multiScattering = false;
+
+            //! Creates a quaterion based on m_rotations
+            AZ::Quaternion GetRotationQuaternion();
+
+            //! Creates a Matrix3x3 based on m_rotations
+            AZ::Matrix3x3 GetRotationMatrix();
+
+            bool GetVaryRadius() { return m_varyRadius && m_count > 1; }
+            bool GetVaryRoughness() { return m_varyRoughness && m_count > 1; }
+            bool GetVaryMetallic() { return m_varyMetallic && m_count > 1; }
+        };
+
+        AZ_DISABLE_COPY_MOVE(AreaLightExampleComponent);
+
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        //! Creates material instances for the given material asset.
+        void InitializeMaterials(AZ::Data::Asset< AZ::RPI::MaterialAsset> materialAsset);
+
+        //! Updates the number of model and asset shown.
+        void UpdateModels(AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset);
+
+        //! Updates material instances based on user config (roughness, metalness etc)
+        void UpdateMaterials();
+
+        //! Updates the lights based on user config (light type, intensity etc)
+        void UpdateLights();
+
+        //! Handles generic properties that apply to all lights.
+        template <typename FeatureProcessorType, typename HandleType>
+        void UpdateLightForType(FeatureProcessorType featureProcessor, HandleType& handle, uint32_t index);
+
+        // Specific light configuration...
+        void UpdatePointLight(PointLightHandle& handle, uint32_t index, AZ::Vector3 position);
+        void UpdateDiskLight(DiskLightHandle& handle, uint32_t index, AZ::Vector3 position);
+        void UpdateCapsuleLight(CapsuleLightHandle& handle, uint32_t index, AZ::Vector3 position);
+        void UpdateQuadLight(QuadLightHandle& handle, uint32_t index, AZ::Vector3 position);
+        void UpdatePolygonLight(PolygonLightHandle& handle, uint32_t index, AZ::Vector3 position);
+
+        //! Gets a 0.0 -> 1.0 value based on index and m_config's m_count.
+        float GetPositionPercentage(uint32_t index);
+
+        //! Gets the model's position based on the index.
+        AZ::Vector3 GetModelPosition(uint32_t index);
+
+        //! Gets the light's position based on the index.
+        AZ::Vector3 GetLightPosition(uint32_t index);
+
+        //! Convience function to either lerp two values or just return the first depending on bool.
+        template<typename T>
+        T GetLerpValue(T values[2], uint32_t index, bool doLerp);
+
+        //! Releases all the models.
+        void ReleaseModels();
+
+        //! Releases all the lights.
+        void ReleaseLights();
+
+        //! Draws all the ImGui controls.
+        void DrawUI();
+
+        //! Draws the lights themselves using AuxGeom
+        void DrawAuxGeom();
+
+        // Transforms the points based on the rotation and translation settings.
+        static void TransformVertices(AZStd::vector<AZ::Vector3>& vertices, const AZ::Quaternion& orientation, const AZ::Vector3& translation);
+
+        // Utility function to get the nth point out of 'count' points on a unit circle on the z plane. Runs counter-clockwise starting from (1.0, 0.0, 0.0).
+        static AZ::Vector3 GetCirclePoint(float n, float count);
+
+        // Calculates the area of a polygon star.
+        static float CalculatePolygonArea(const AZStd::vector<AZ::Vector3>& vertices);
+
+        // Gets the edge vertices for a polygon star on the z plane.
+        static AZStd::vector<AZ::Vector3> GetPolygonVertices(uint32_t pointCount, float minMaxRadius[2]);
+
+        // Gets triangles for a polygon star on the z plane.
+        static AZStd::vector<AZ::Vector3> GetPolygonTriangles(uint32_t pointCount, float minMaxRadius[2]);
+
+        Configuration m_config;
+
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_modelAsset;
+        AZ::RPI::AuxGeomDrawPtr m_auxGeom;
+
+        // Feature processors
+        AZ::Render::MeshFeatureProcessorInterface* m_meshFeatureProcessor = nullptr;
+        AZ::Render::PointLightFeatureProcessorInterface* m_pointLightFeatureProcessor = nullptr;
+        AZ::Render::DiskLightFeatureProcessorInterface* m_diskLightFeatureProcessor = nullptr;
+        AZ::Render::CapsuleLightFeatureProcessorInterface* m_capsuleLightFeatureProcessor = nullptr;
+        AZ::Render::QuadLightFeatureProcessorInterface* m_quadLightFeatureProcessor = nullptr;
+        AZ::Render::PolygonLightFeatureProcessorInterface* m_polygonLightFeatureProcessor = nullptr;
+        AZ::Render::SkyBoxFeatureProcessorInterface* m_skyBoxFeatureProcessor = nullptr;
+
+        AZ::RPI::MaterialPropertyIndex m_roughnessPropertyIndex;
+        AZ::RPI::MaterialPropertyIndex m_metallicPropertyIndex;
+        AZ::RPI::MaterialPropertyIndex m_multiScatteringEnabledIndex;
+
+        AZStd::vector<MaterialInstance> m_materialInstances;
+        AZStd::vector<MeshHandle> m_meshHandles;
+        AZStd::vector<LightHandle> m_lightHandles;
+
+        AZ::Render::PhotometricValue m_photometricValue;
+
+        ImGuiSidebar m_imguiSidebar;
+        ImGuiAssetBrowser m_materialBrowser;
+        ImGuiAssetBrowser m_modelBrowser;
+        ImGuiAssetBrowser::WidgetSettings m_materialBrowserSettings;
+        ImGuiAssetBrowser::WidgetSettings m_modelBrowserSettings;
+
+        bool m_materialsNeedUpdate = true;
+    };
+}

+ 368 - 0
Gem/Code/Source/AssetLoadTestComponent.cpp

@@ -0,0 +1,368 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <AssetLoadTestComponent.h>
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <Automation/ScriptRunnerBus.h>
+
+#include <AzCore/Debug/EventTrace.h>
+#include <AzCore/Serialization/SerializeContext.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+
+    void AssetLoadTestComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<AssetLoadTestComponent, EntityLatticeTestComponent>()
+                ->Version(0)
+                ;
+        }
+    }
+
+
+    AssetLoadTestComponent::AssetLoadTestComponent() 
+        : m_materialBrowser("@user@/AssetLoadTestComponent/material_browser.xml")
+        , m_modelBrowser("@user@/AssetLoadTestComponent/model_browser.xml")
+        , m_imguiSidebar("@user@/AssetLoadTestComponent/sidebar.xml")
+    {
+        m_sampleName = "AssetLoadTestComponent";
+
+        m_materialBrowser.SetFilter([](const AZ::Data::AssetInfo& assetInfo)
+        {
+            return assetInfo.m_assetType == azrtti_typeid<AZ::RPI::MaterialAsset>();
+        });
+
+        m_modelBrowser.SetFilter([](const AZ::Data::AssetInfo& assetInfo)
+        {
+            return assetInfo.m_assetType == azrtti_typeid<AZ::RPI::ModelAsset>();
+        });
+
+        const AZStd::vector<AZStd::string> defaultMaterialAllowlist =
+        {
+            "materials/defaultpbr.azmaterial",
+            "materials/presets/pbr/metal_aluminum_polished.azmaterial",
+            "shaders/staticmesh_colorr.azmaterial",
+            "shaders/staticmesh_colorg.azmaterial",
+            "shaders/staticmesh_colorb.azmaterial"
+        };
+        m_materialBrowser.SetDefaultPinnedAssets(defaultMaterialAllowlist);
+
+        m_pinnedMaterialCount = m_materialBrowser.GetPinnedAssets().size();
+
+        const AZStd::vector<AZStd::string> defaultModelAllowist =
+        {
+            "Objects/bunny.azmodel",
+            "Objects/Shaderball_simple.azmodel",
+            "Objects/suzanne.azmodel",
+        };
+        m_modelBrowser.SetDefaultPinnedAssets(defaultModelAllowist);
+    }
+
+    void AssetLoadTestComponent::Activate()
+    {
+        AZ::TickBus::Handler::BusConnect();
+
+        m_imguiSidebar.Activate();
+        m_materialBrowser.Activate();
+        m_modelBrowser.Activate();
+
+        if (!m_materialBrowser.IsConfigFileLoaded())
+        {
+            AZ_TracePrintf("AssetLoadTestComponent", "Material allow list not loaded. Defaulting to built in list.\n");
+
+            m_materialBrowser.ResetPinnedAssetsToDefault();
+        }
+
+        if (!m_modelBrowser.IsConfigFileLoaded())
+        {
+            AZ_TracePrintf("AssetLoadTestComponent", "Model allow list not loaded. Defaulting to built in list.\n");
+
+            m_modelBrowser.ResetPinnedAssetsToDefault();
+        }
+
+        Base::Activate();
+    }
+
+    void AssetLoadTestComponent::Deactivate()
+    {
+        m_materialBrowser.Deactivate();
+        m_modelBrowser.Deactivate();
+
+        AZ::TickBus::Handler::BusDisconnect();
+        m_imguiSidebar.Deactivate();
+        Base::Deactivate();
+    }
+    
+    void AssetLoadTestComponent::PrepareCreateLatticeInstances(uint32_t instanceCount)
+    {
+        m_modelInstanceData.reserve(instanceCount);
+    }
+
+    void AssetLoadTestComponent::CreateLatticeInstance(const AZ::Transform& transform)
+    {
+        m_modelInstanceData.emplace_back<ModelInstanceData>({});
+        ModelInstanceData& data = m_modelInstanceData.back();
+        data.m_modelAssetId = GetRandomModelId();
+        data.m_materialAssetId = GetRandomMaterialId();
+        data.m_transform = transform;
+    }
+
+    void AssetLoadTestComponent::FinalizeLatticeInstances()
+    {
+        AZStd::set<AZ::Data::AssetId> assetIds;
+
+        for (ModelInstanceData& instanceData : m_modelInstanceData)
+        {
+            if (instanceData.m_materialAssetId.IsValid())
+            {
+                assetIds.insert(instanceData.m_materialAssetId);
+            }
+
+            if (instanceData.m_modelAssetId.IsValid())
+            {
+                assetIds.insert(instanceData.m_modelAssetId);
+            }
+        }
+
+        AZStd::vector<AZ::AssetCollectionAsyncLoader::AssetToLoadInfo> assetList;
+
+        for (auto& assetId : assetIds)
+        {
+            AZ::Data::AssetInfo assetInfo;
+            AZ::Data::AssetCatalogRequestBus::BroadcastResult(assetInfo, &AZ::Data::AssetCatalogRequests::GetAssetInfoById, assetId);
+            if (assetInfo.m_assetId.IsValid())
+            {
+                AZ::AssetCollectionAsyncLoader::AssetToLoadInfo info;
+                info.m_assetPath = assetInfo.m_relativePath;
+                info.m_assetType = assetInfo.m_assetType;
+                assetList.push_back(info);
+            }
+        }
+
+        PreloadAssets(assetList);
+
+        // pause script and tick until assets are ready
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScriptWithTimeout, 120.0f);
+        AZ::TickBus::Handler::BusDisconnect();
+    }
+
+    void AssetLoadTestComponent::OnAllAssetsReadyActivate()
+    {
+        AZ::Render::MaterialAssignmentMap materials;
+        for (ModelInstanceData& instanceData : m_modelInstanceData)
+        {
+            AZ::Render::MaterialAssignment& defaultAssignment = materials[AZ::Render::DefaultMaterialAssignmentId];
+            defaultAssignment = {};
+
+            if (instanceData.m_materialAssetId.IsValid())
+            {
+                defaultAssignment.m_materialAsset.Create(instanceData.m_materialAssetId);
+                defaultAssignment.m_materialInstance = AZ::RPI::Material::FindOrCreate(defaultAssignment.m_materialAsset);
+
+                // cache the material when its loaded
+                m_cachedMaterials.insert(defaultAssignment.m_materialAsset);
+            }
+
+            if (instanceData.m_modelAssetId.IsValid())
+            {
+                AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset;
+                modelAsset.Create(instanceData.m_modelAssetId);
+
+                instanceData.m_meshHandle = GetMeshFeatureProcessor()->AcquireMesh(modelAsset, materials);
+                GetMeshFeatureProcessor()->SetTransform(instanceData.m_meshHandle, instanceData.m_transform);
+            }
+        }
+
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void AssetLoadTestComponent::DestroyLatticeInstances()
+    {
+        DestroyHandles();
+        m_modelInstanceData.clear();
+    }
+
+    void AssetLoadTestComponent::DestroyHandles()
+    {
+        for (ModelInstanceData& instanceData : m_modelInstanceData)
+        {
+            GetMeshFeatureProcessor()->ReleaseMesh(instanceData.m_meshHandle);
+            instanceData.m_meshHandle = {};
+        }
+    }
+
+    AZ::Data::AssetId AssetLoadTestComponent::GetRandomModelId() const
+    {
+        auto& modelAllowlist = m_modelBrowser.GetPinnedAssets();
+
+        if (modelAllowlist.size())
+        {
+            const size_t randomModelIndex = rand() % modelAllowlist.size();
+            return modelAllowlist[randomModelIndex].m_assetId;
+        }
+        else
+        {
+            return AZ::RPI::AssetUtils::GetAssetIdForProductPath("testdata/objects/cube/cube.azmodel", AZ::RPI::AssetUtils::TraceLevel::Error);
+        }
+    }
+
+    AZ::Data::AssetId AssetLoadTestComponent::GetRandomMaterialId() const
+    {
+        auto& materialAllowlist = m_materialBrowser.GetPinnedAssets();
+
+        if (materialAllowlist.size())
+        {
+            const size_t randomMaterialIndex = rand() % materialAllowlist.size();
+            return materialAllowlist[randomMaterialIndex].m_assetId;
+        }
+        else
+        {
+            return AZ::RPI::AssetUtils::GetAssetIdForProductPath("shaders/staticmesh.azmaterial", AZ::RPI::AssetUtils::TraceLevel::Error);
+        }
+    }
+
+
+    void AssetLoadTestComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint scriptTime)
+    {
+        AZ_TRACE_METHOD();
+
+        const float timeSeconds = static_cast<float>(scriptTime.GetSeconds());
+
+        if (m_lastMaterialSwitchInSeconds == 0 || m_lastModelSwitchInSeconds == 0)
+        {
+            m_lastMaterialSwitchInSeconds = timeSeconds;
+            m_lastModelSwitchInSeconds = timeSeconds;
+            return;
+        }
+
+        bool materialSwitchRequested = m_materialSwitchEnabled && timeSeconds - m_lastMaterialSwitchInSeconds >= m_materialSwitchTimeInSeconds;
+        bool modelSwitchRequested = m_modelSwitchEnabled && timeSeconds - m_lastModelSwitchInSeconds >= m_modelSwitchTimeInSeconds;
+
+        if (m_updateTransformEnabled)
+        {
+            float radians = static_cast<float>(fmod(scriptTime.GetSeconds(), AZ::Constants::TwoPi));
+            AZ::Vector3 rotation(radians, radians, radians);
+            AZ::Transform rotationTransform;
+            rotationTransform.SetFromEulerRadians(rotation);
+
+            for (ModelInstanceData& instanceData : m_modelInstanceData)
+            {
+                GetMeshFeatureProcessor()->SetTransform(instanceData.m_meshHandle, instanceData.m_transform * rotationTransform);
+            }
+        }
+
+        bool materialsChanged = false;
+        bool modelsChanged = false;
+
+        if (m_imguiSidebar.Begin())
+        {
+            ImGui::Checkbox("Switch Materials Every N Seconds", &m_materialSwitchEnabled);
+            ImGui::SliderFloat("##MaterialSwitchTime", &m_materialSwitchTimeInSeconds, 0.1f, 10.0f);
+
+            ImGui::Spacing();
+
+            ImGui::Checkbox("Switch Models Every N Seconds", &m_modelSwitchEnabled);
+            ImGui::SliderFloat("##ModelSwitchTime", &m_modelSwitchTimeInSeconds, 0.1f, 10.0f);
+
+            ImGui::Spacing();
+            ImGui::Checkbox("Update Transforms Every Frame", &m_updateTransformEnabled);
+
+            ImGui::Spacing();
+            ImGui::Separator();
+            ImGui::Spacing();
+
+            RenderImGuiLatticeControls();
+
+            ImGui::Spacing();
+            ImGui::Separator();
+            ImGui::Spacing();
+
+            ImGuiAssetBrowser::WidgetSettings assetBrowserSettings;
+            assetBrowserSettings.m_labels.m_pinnedAssetList = "Allow List";
+            assetBrowserSettings.m_labels.m_pinButton = "Add To Allow List";
+            assetBrowserSettings.m_labels.m_unpinButton = "Remove From Allow List";
+
+            assetBrowserSettings.m_labels.m_root = "Materials";
+            m_materialBrowser.Tick(assetBrowserSettings);
+
+            auto& pinnedMaterials = m_materialBrowser.GetPinnedAssets();
+            materialsChanged = pinnedMaterials.size() != m_pinnedMaterialCount;
+
+            if (materialsChanged)
+            {
+                m_pinnedMaterialCount = pinnedMaterials.size();
+                // Keep the current m_cachedMaterials to avoid release-load the same material
+                MaterialAssetSet newCache;
+                // clean up cached material which refcount is 1
+                for (auto& pinnedMaterial : pinnedMaterials)
+                {
+                    AZ::Data::AssetId materialAssetid = pinnedMaterial.m_assetId;
+                    // Cache the asset if it's loaded
+                    AZ::Data::Asset<AZ::RPI::MaterialAsset> asset = AZ::Data::AssetManager::Instance().FindAsset<AZ::RPI::MaterialAsset>(materialAssetid, AZ::Data::AssetLoadBehavior::PreLoad);
+                    if (asset.IsReady())
+                    {
+                        newCache.insert(asset);
+                    }
+                }
+                m_cachedMaterials = newCache;
+            }
+
+            ImGui::Spacing();
+            ImGui::Separator();
+            ImGui::Spacing();
+
+            assetBrowserSettings.m_labels.m_root = "Models";
+            m_modelBrowser.Tick(assetBrowserSettings);
+            modelsChanged = m_lastPinnedModelCount != m_modelBrowser.GetPinnedAssets().size();
+
+            m_imguiSidebar.End();
+        }
+
+        if (materialSwitchRequested || materialsChanged)
+        {
+            for (ModelInstanceData& instanceData : m_modelInstanceData)
+            {
+                instanceData.m_materialAssetId = GetRandomMaterialId();
+            }
+            m_lastMaterialSwitchInSeconds = timeSeconds;
+        }
+
+        if (modelSwitchRequested || modelsChanged)
+        {
+            m_lastPinnedModelCount = m_modelBrowser.GetPinnedAssets().size();
+            for (ModelInstanceData& instanceData : m_modelInstanceData)
+            {
+                instanceData.m_modelAssetId = GetRandomModelId();
+            }
+            m_lastModelSwitchInSeconds = timeSeconds;
+        }
+
+        if (materialSwitchRequested || materialsChanged || modelSwitchRequested || modelsChanged)
+        {
+            DestroyHandles();
+            FinalizeLatticeInstances();
+        }
+    }
+
+} // namespace AtomSampleViewer

+ 107 - 0
Gem/Code/Source/AssetLoadTestComponent.h

@@ -0,0 +1,107 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <EntityLatticeTestComponent.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/ImGuiAssetBrowser.h>
+#include <AzCore/Component/TickBus.h>
+
+namespace AtomSampleViewer
+{
+    /*
+        This test loads a configurable lattice of entities and swaps out each entity's model and material
+        at given time steps. Each entity can have its assets swapped very rapidly (10ths of seconds).
+
+        The assets that are applied to the entities are chosen from a configurable 
+        "allow-list" of models and materials. The allow-list is saved to the cache's user folder and 
+        loaded on startup. This makes it easy to chose "working" assets to use in the test vs more development 
+        assets that may not be working properly. It also allows you to build cases where we want
+        to test instancing more than loading. UI to modify allow-list is a core part of this component.
+    */
+    class AssetLoadTestComponent final
+        : public EntityLatticeTestComponent
+        , public AZ::TickBus::Handler
+    {
+        using Base = EntityLatticeTestComponent;
+
+    public:
+        AZ_COMPONENT(AssetLoadTestComponent, "{30E6EE46-2CD5-4903-801F-56AE70A33656}", EntityLatticeTestComponent);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        AssetLoadTestComponent();
+        
+        //! AZ::Component overrides...
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        AZ_DISABLE_COPY_MOVE(AssetLoadTestComponent);
+
+        // CommonSampleComponentBase overrides...
+        void OnAllAssetsReadyActivate() override;
+
+        //! EntityLatticeTestComponent overrides...
+        void PrepareCreateLatticeInstances(uint32_t instanceCount) override;
+        void CreateLatticeInstance(const AZ::Transform& transform) override;
+        void FinalizeLatticeInstances() override;
+        void DestroyLatticeInstances() override;
+
+        void DestroyHandles();
+
+        AZ::Data::AssetId GetRandomModelId() const;
+        AZ::Data::AssetId GetRandomMaterialId() const;
+
+        void OnTick(float deltaTime, AZ::ScriptTimePoint scriptTime) override;
+
+        struct ModelInstanceData
+        {
+            AZ::Transform m_transform;
+            AZ::Data::AssetId m_modelAssetId;
+            AZ::Data::AssetId m_materialAssetId;
+            AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+        };
+
+        ImGuiSidebar m_imguiSidebar;
+        ImGuiAssetBrowser m_materialBrowser;
+        ImGuiAssetBrowser m_modelBrowser;
+        
+        AZStd::vector<ModelInstanceData> m_modelInstanceData;
+
+        struct Compare
+        {
+            bool operator()(const AZ::Data::Asset<AZ::RPI::MaterialAsset>& lhs, const AZ::Data::Asset<AZ::RPI::MaterialAsset>& rhs) const
+            {
+                if (lhs.GetId().m_guid == rhs.GetId().m_guid)
+                {
+                    return lhs.GetId().m_subId > rhs.GetId().m_subId;
+                }
+                return lhs.GetId().m_guid > rhs.GetId().m_guid;
+            }
+        };
+
+        using MaterialAssetSet = AZStd::set<AZ::Data::Asset<AZ::RPI::MaterialAsset>, Compare>;
+        MaterialAssetSet m_cachedMaterials;
+        uint32_t m_pinnedMaterialCount = 0;
+        
+        size_t m_lastPinnedModelCount = 0;
+        float m_lastMaterialSwitchInSeconds = 0;
+        float m_lastModelSwitchInSeconds = 0;
+        float m_materialSwitchTimeInSeconds = 5.0f;
+        float m_modelSwitchTimeInSeconds = 3.0f;
+        bool m_materialSwitchEnabled = true;
+        bool m_modelSwitchEnabled = true;
+        bool m_updateTransformEnabled = false;
+    };
+} // namespace AtomSampleViewer

+ 183 - 0
Gem/Code/Source/AtomSampleViewerModule.cpp

@@ -0,0 +1,183 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <AzCore/Module/Module.h>
+#include <AzCore/Memory/SystemAllocator.h>
+
+#include <AreaLightExampleComponent.h>
+#include <AssetLoadTestComponent.h>
+#include <AuxGeomExampleComponent.h>
+#include <AtomSampleViewerSystemComponent.h>
+#include <BistroBenchmarkComponent.h>
+#include <BloomExampleComponent.h>
+#include <CheckerboardExampleComponent.h>
+#include <CullingAndLodExampleComponent.h>
+#include <MultiRenderPipelineExampleComponent.h>
+#include <MultiSceneExampleComponent.h>
+#include <MultiViewSingleSceneAuxGeomExampleComponent.h>
+#include <DepthOfFieldExampleComponent.h>
+#include <DecalExampleComponent.h>
+#include <DynamicDrawExampleComponent.h>
+#include <DynamicMaterialTestComponent.h>
+#include <MaterialHotReloadTestComponent.h>
+#include <ExposureExampleComponent.h>
+#include <LightCullingExampleComponent.h>
+#include <MeshExampleComponent.h>
+#include <MSAA_RPI_ExampleComponent.h>
+#include <ParallaxMappingExampleComponent.h>
+#include <SampleComponentManager.h>
+#include <SceneReloadSoakTestComponent.h>
+#include <ShadingExampleComponent.h>
+#include <ShadowExampleComponent.h>
+#include <ShadowedBistroExampleComponent.h>
+#include <SkinnedMeshExampleComponent.h>
+#include <SsaoExampleComponent.h>
+#include <StreamingImageExampleComponent.h>
+#include <RootConstantsExampleComponent.h>
+#include <TonemappingExampleComponent.h>
+#include <TransparencyExampleComponent.h>
+#include <DiffuseGIExampleComponent.h>
+#include <SSRExampleComponent.h>
+
+#include <RHI/AlphaToCoverageExampleComponent.h>
+#include <RHI/AsyncComputeExampleComponent.h>
+#include <RHI/BindlessPrototypeExampleComponent.h>
+#include <RHI/ComputeExampleComponent.h>
+#include <RHI/CopyQueueComponent.h>
+#include <RHI/IndirectRenderingExampleComponent.h>
+#include <RHI/InputAssemblyExampleComponent.h>
+#include <RHI/SubpassExampleComponent.h>
+#include <RHI/DualSourceBlendingComponent.h>
+#include <RHI/MRTExampleComponent.h>
+#include <RHI/MSAAExampleComponent.h>
+#include <RHI/MultiThreadComponent.h>
+#include <RHI/MultiViewportSwapchainComponent.h>
+#include <RHI/StencilExampleComponent.h>
+#include <RHI/MultipleViewsComponent.h>
+#include <RHI/QueryExampleComponent.h>
+#include <RHI/SwapchainExampleComponent.h>
+#include <RHI/SphericalHarmonicsExampleComponent.h>
+#include <RHI/Texture3dExampleComponent.h>
+#include <RHI/TextureArrayExampleComponent.h>
+#include <RHI/TextureExampleComponent.h>
+#include <RHI/TextureMapExampleComponent.h>
+#include <RHI/TriangleExampleComponent.h>
+#include <RHI/TrianglesConstantBufferExampleComponent.h>
+#include <RHI/RayTracingExampleComponent.h>
+#include <AzFramework/Scene/SceneSystemComponent.h>
+
+#include <Atom/Feature/SkinnedMesh/SkinnedMeshInputBuffers.h>
+
+namespace AtomSampleViewer
+{
+    class Module final
+        : public AZ::Module
+    {
+    public:
+        AZ_CLASS_ALLOCATOR(Module, AZ::SystemAllocator, 0);
+        AZ_RTTI(Module, "{8FEB7E9B-A5F7-4917-A1DE-974DE1FA7F1E}", AZ::Module);
+
+        Module()
+        {
+            m_descriptors.insert(m_descriptors.end(), {
+                AtomSampleViewerSystemComponent::CreateDescriptor(),
+                SampleComponentManager::CreateDescriptor(),
+                });
+
+            // RHI Samples
+            m_descriptors.insert(m_descriptors.end(), {
+                AlphaToCoverageExampleComponent::CreateDescriptor(),
+                AsyncComputeExampleComponent::CreateDescriptor(),
+                BindlessPrototypeExampleComponent::CreateDescriptor(),
+                ComputeExampleComponent::CreateDescriptor(),
+                CopyQueueComponent::CreateDescriptor(),
+                DualSourceBlendingComponent::CreateDescriptor(),
+                IndirectRenderingExampleComponent::CreateDescriptor(),
+                InputAssemblyExampleComponent::CreateDescriptor(),
+                SubpassExampleComponent::CreateDescriptor(),
+                MRTExampleComponent::CreateDescriptor(),
+                MSAAExampleComponent::CreateDescriptor(),
+                MultiThreadComponent::CreateDescriptor(),
+                MultipleViewsComponent::CreateDescriptor(),
+                MultiViewportSwapchainComponent::CreateDescriptor(),
+                QueryExampleComponent::CreateDescriptor(),
+                StencilExampleComponent::CreateDescriptor(),
+                SwapchainExampleComponent::CreateDescriptor(),
+                SphericalHarmonicsExampleComponent::CreateDescriptor(),
+                Texture3dExampleComponent::CreateDescriptor(),
+                TextureArrayExampleComponent::CreateDescriptor(),
+                TextureExampleComponent::CreateDescriptor(),
+                TextureMapExampleComponent::CreateDescriptor(),
+                TriangleExampleComponent::CreateDescriptor(),
+                TrianglesConstantBufferExampleComponent::CreateDescriptor(),
+                RayTracingExampleComponent::CreateDescriptor()
+                });
+
+            // RPI Samples
+            m_descriptors.insert(m_descriptors.end(), {
+                AreaLightExampleComponent::CreateDescriptor(),
+                AssetLoadTestComponent::CreateDescriptor(),
+                BistroBenchmarkComponent::CreateDescriptor(),
+                BloomExampleComponent::CreateDescriptor(),
+                CheckerboardExampleComponent::CreateDescriptor(),
+                CullingAndLodExampleComponent::CreateDescriptor(),
+                MultiRenderPipelineExampleComponent::CreateDescriptor(),
+                MultiSceneExampleComponent::CreateDescriptor(),
+                MultiViewSingleSceneAuxGeomExampleComponent::CreateDescriptor(),
+                DecalExampleComponent::CreateDescriptor(),
+                DepthOfFieldExampleComponent::CreateDescriptor(),
+                DynamicMaterialTestComponent::CreateDescriptor(),
+                MaterialHotReloadTestComponent::CreateDescriptor(),
+                ExposureExampleComponent::CreateDescriptor(),
+                MeshExampleComponent::CreateDescriptor(),
+                DynamicDrawExampleComponent::CreateDescriptor(),
+                SceneReloadSoakTestComponent::CreateDescriptor(),
+                ShadingExampleComponent::CreateDescriptor(),
+                ShadowExampleComponent::CreateDescriptor(),
+                ShadowedBistroExampleComponent::CreateDescriptor(),
+                SkinnedMeshExampleComponent::CreateDescriptor(),
+                SsaoExampleComponent::CreateDescriptor(),
+                LightCullingExampleComponent::CreateDescriptor(),
+                StreamingImageExampleComponent::CreateDescriptor(),
+                AuxGeomExampleComponent::CreateDescriptor(),
+                MSAA_RPI_ExampleComponent::CreateDescriptor(),
+                RootConstantsExampleComponent::CreateDescriptor(),
+                TonemappingExampleComponent::CreateDescriptor(),
+                TransparencyExampleComponent::CreateDescriptor(),
+                ParallaxMappingExampleComponent::CreateDescriptor(),
+                DiffuseGIExampleComponent::CreateDescriptor(),
+                SSRExampleComponent::CreateDescriptor(),
+                });
+        }
+
+        ~Module() override = default;
+
+        AZ::ComponentTypeList GetRequiredSystemComponents() const override
+        {
+            AZ::ComponentTypeList requiredComponents;
+    
+            requiredComponents = 
+            {
+                azrtti_typeid<AzFramework::SceneSystemComponent>(),
+                azrtti_typeid<AtomSampleViewerSystemComponent>(),
+                azrtti_typeid<SampleComponentManager>()
+            };
+    
+            return requiredComponents;
+        }
+    };
+} // namespace AtomSampleViewer
+
+// DO NOT MODIFY THIS LINE UNLESS YOU RENAME THE GEM
+// The first parameter should be GemName_GemIdLower
+// The second should be the fully qualified name of the class above
+AZ_DECLARE_MODULE_CLASS(Gem_AtomSampleViewer, AtomSampleViewer::Module)

+ 18 - 0
Gem/Code/Source/AtomSampleViewerOptions.h

@@ -0,0 +1,18 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+namespace AtomSampleViewer
+{
+    bool SupportsMultipleWindows();
+} // namespace AtomSampleViewer

+ 27 - 0
Gem/Code/Source/AtomSampleViewerRequestBus.h

@@ -0,0 +1,27 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+#pragma once
+
+#include <AzCore/EBus/EBus.h>
+
+namespace AtomSampleViewer
+{
+    class AtomSampleViewerRequests
+        : public AZ::EBusTraits
+    {
+    public:
+        //! Return the specified exit code when exiting AtomSampleViewer
+        virtual void SetExitCode(int exitCode) = 0;
+    };
+    using AtomSampleViewerRequestsBus = AZ::EBus<AtomSampleViewerRequests>;
+
+} // namespace AtomSampleViewer

+ 233 - 0
Gem/Code/Source/AtomSampleViewerSystemComponent.cpp

@@ -0,0 +1,233 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <AtomSampleViewerSystemComponent.h>
+#include <MaterialFunctors/StacksShaderCollectionFunctor.h>
+#include <MaterialFunctors/StacksShaderInputFunctor.h>
+#include <Automation/ImageComparisonConfig.h>
+
+#include <EntityLatticeTestComponent.h>
+
+#include <AzCore/Asset/AssetManagerBus.h>
+#include <AzCore/Asset/AssetManager.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/Component/Entity.h>
+#include <AzCore/IO/SystemFile.h>
+
+#include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h>
+#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
+
+#include <Atom/Bootstrap/DefaultWindowBus.h>
+
+#include <Atom/RHI/Factory.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Shader/Metrics/ShaderMetricsSystem.h>
+
+#include <ISystem.h>
+#include <IConsole.h>
+
+#include <Utils/ImGuiAssetBrowser.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/ImGuiSaveFilePath.h>
+#include <Utils/Utils.h>
+
+namespace AtomSampleViewer
+{
+    void AtomSampleViewerSystemComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<AtomSampleViewerSystemComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+
+        PerfMetrics::Reflect(context);
+        ImGuiAssetBrowser::Reflect(context);
+        ImGuiSidebar::Reflect(context);
+        ImGuiSaveFilePath::Reflect(context);
+        StacksShaderCollectionFunctor::Reflect(context);
+        StacksShaderInputFunctor::Reflect(context);
+
+        ImageComparisonConfig::Reflect(context);
+    }
+
+    void AtomSampleViewerSystemComponent::PerfMetrics::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<PerfMetrics>()
+                ->Version(1)
+                ->Field("TestDurationSeconds", &PerfMetrics::m_timingTargetSeconds)
+                ->Field("AverageFramesPerSecond", &PerfMetrics::m_averageDeltaSeconds)
+                ->Field("SecondsToRender", &PerfMetrics::m_timeToFirstRenderSeconds)
+                ;
+        }
+
+        // Abstract base component is used by multiple components and needs to be reflected in a single location.
+        EntityLatticeTestComponent::Reflect(context);
+    }
+
+
+    void AtomSampleViewerSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
+    {
+        provided.push_back(AZ_CRC("PrototypeLmbrCentralService", 0xe35e6de0));
+    }
+
+    void AtomSampleViewerSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
+    {
+        required.push_back(AZ::RHI::Factory::GetComponentService());
+        required.push_back(AZ_CRC("AssetDatabaseService", 0x3abf5601));
+        required.push_back(AZ_CRC("RPISystem", 0xf2add773));
+        required.push_back(AZ_CRC("BootstrapSystemComponent", 0xb8f32711));
+    }
+
+    AtomSampleViewerSystemComponent::AtomSampleViewerSystemComponent()
+        : m_timestamp(HighResTimer::now())
+    {
+        m_atomSampleViewerEntity = aznew AZ::Entity();
+        m_atomSampleViewerEntity->Init();
+    }
+
+    AtomSampleViewerSystemComponent::~AtomSampleViewerSystemComponent()
+    {
+    }
+
+    void AtomSampleViewerSystemComponent::Activate()
+    {
+        AZ::EntitySystemBus::Handler::BusConnect();
+
+        AZ::ApplicationTypeQuery appType;
+        AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationBus::Events::QueryApplicationType, appType);
+        if (appType.IsValid() && !appType.IsEditor())
+        {
+            // AtomSampleViewer SampleComponentManager creates and manages its own scene and render pipelines. 
+            // We disable the creation of default scene in BootStrapSystemComponent
+            AZ::Render::Bootstrap::DefaultWindowBus::Broadcast(&AZ::Render::Bootstrap::DefaultWindowBus::Events::SetCreateDefaultScene, false);
+        }
+
+        AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::LoadCatalog, "@assets@/assetcatalog.xml");
+
+        m_atomSampleViewerEntity->Activate();
+
+        AZ::TickBus::Handler::BusConnect();
+        CrySystemEventBus::Handler::BusConnect();
+    }
+
+    void AtomSampleViewerSystemComponent::Deactivate()
+    {
+        CrySystemEventBus::Handler::BusDisconnect();
+        AZ::TickBus::Handler::BusDisconnect();
+
+        if (m_atomSampleViewerEntity != nullptr)
+        {
+            m_atomSampleViewerEntity->Deactivate();
+        }
+
+        AZ::EntitySystemBus::Handler::BusDisconnect();
+    }
+
+    void AtomSampleViewerSystemComponent::OnEntityDestroyed(const AZ::EntityId& entityId)
+    {
+        if (m_atomSampleViewerEntity && m_atomSampleViewerEntity->GetId() == entityId)
+        {
+            m_atomSampleViewerEntity = nullptr;
+        }
+    }
+
+    void AtomSampleViewerSystemComponent::OnTick(float deltaTime, AZ::ScriptTimePoint time)
+    {
+        AZ_UNUSED(time);
+        TickTimeoutShutdown(deltaTime);
+
+#if defined(AZ_DEBUG_BUILD)
+        TrackPerfMetrics(deltaTime);
+#endif
+    }
+
+    void AtomSampleViewerSystemComponent::OnCrySystemInitialized(ISystem& system, const SSystemInitParams&)
+    {
+        system.GetIConsole()->GetCVar("sys_asserts")->Set(2);
+
+        // Currently CSystem::Init hides and constrains the mouse cursor.
+        // For AtomSampleViewer we want it visible so that we can use the ImGui menus
+        AzFramework::InputSystemCursorRequestBus::Event(AzFramework::InputDeviceMouse::Id,
+                                                        &AzFramework::InputSystemCursorRequests::SetSystemCursorState,
+                                                        AzFramework::SystemCursorState::UnconstrainedAndVisible);
+
+        ReadTimeoutShutdown();
+    }
+
+    void AtomSampleViewerSystemComponent::ReadTimeoutShutdown()
+    {
+        const AzFramework::CommandLine* commandLine = nullptr;
+        AzFramework::ApplicationRequests::Bus::BroadcastResult(commandLine, &AzFramework::ApplicationRequests::Bus::Events::GetApplicationCommandLine);
+        if (commandLine)
+        {
+            if (commandLine->HasSwitch("timeout"))
+            {
+                const AZStd::string& timeoutValue = commandLine->GetSwitchValue("timeout", 0);
+                const float timeoutInSeconds = static_cast<float>(atoi(timeoutValue.c_str()));
+                AZ_Printf("AtomSampleViewer", "starting up with timeout shutdown of %f seconds", timeoutInSeconds);
+                m_secondsBeforeShutdown = timeoutInSeconds;
+            }
+        }
+    }
+
+    void AtomSampleViewerSystemComponent::TickTimeoutShutdown(float deltaTimeInSeconds)
+    {
+        if (m_secondsBeforeShutdown > 0.f)
+        {
+            m_secondsBeforeShutdown -= deltaTimeInSeconds;
+            if (m_secondsBeforeShutdown <= 0.f)
+            {
+                AZ_Printf("AtomSampleViewer", "Timeout reached, shutting down");
+                AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop); // or ::TerminateOnError for a more forceful option
+            }
+        }
+    }
+
+    void AtomSampleViewerSystemComponent::TrackPerfMetrics(const float deltaTime)
+    {
+        m_frameCount++;
+
+        if (m_frameCount != 1) // don't accumulate delta on the first frame
+        {
+            m_accumulatedDeltaSeconds += deltaTime;
+        }
+
+        if (!m_testsLogged)
+        {
+            AZStd::chrono::duration<float> elapsedTime = HighResTimer::now() - m_timestamp;
+
+            if (m_frameCount == 1)
+            {
+                m_perfMetrics.m_timeToFirstRenderSeconds = elapsedTime.count();
+            }
+
+            if (elapsedTime.count() >= m_perfMetrics.m_timingTargetSeconds)
+            {
+                if (m_frameCount > 1)
+                {
+                    m_perfMetrics.m_averageDeltaSeconds = m_accumulatedDeltaSeconds / static_cast<float>(m_frameCount - 1);
+                }
+                LogPerfMetrics();
+                m_testsLogged = true;
+            }
+        }
+    }
+
+    void AtomSampleViewerSystemComponent::LogPerfMetrics() const
+    {
+        AZ::Utils::SaveObjectToFile("metrics.xml", AZ::DataStream::ST_XML, &m_perfMetrics);
+    }
+} // namespace AtomSampleViewer

+ 88 - 0
Gem/Code/Source/AtomSampleViewerSystemComponent.h

@@ -0,0 +1,88 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/Component/Component.h>
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/RTTI/RTTI.h>
+#include <AzCore/Component/EntityBus.h>
+
+#include <Atom/RPI.Reflect/Shader/ShaderAsset.h>
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+
+#include <CrySystemBus.h>
+
+namespace AtomSampleViewer
+{
+    using HighResTimer = AZStd::chrono::high_resolution_clock;
+
+    class AtomSampleViewerSystemComponent final
+        : public AZ::Component
+        , public AZ::TickBus::Handler
+        , public AZ::EntitySystemBus::Handler
+        , protected CrySystemEventBus::Handler
+    {
+    public:
+        AZ_COMPONENT(AtomSampleViewerSystemComponent, "{714873AD-70FC-47A8-A609-46103CB8DABA}");
+
+        static void Reflect(AZ::ReflectContext* context);
+        static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
+        static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
+
+        AtomSampleViewerSystemComponent();
+        ~AtomSampleViewerSystemComponent() override;
+
+        void Activate() override;
+        void Deactivate() override;
+
+        // AZ::EntitySystemBus::Handler
+        void OnEntityDestroyed(const AZ::EntityId& entityId) override;
+
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+        // CrySystemEventBus::Handler
+        void OnCrySystemInitialized(ISystem& system, const SSystemInitParams&) override;
+
+    private:
+        void TrackPerfMetrics(const float deltaTime);
+        void LogPerfMetrics() const;
+
+        struct PerfMetrics
+        {
+            AZ_CLASS_ALLOCATOR(PerfMetrics, AZ::SystemAllocator, 0);
+            AZ_TYPE_INFO(PerfMetrics, "{D333FB60-DC06-42CF-9124-B1EF90690A16}");
+
+            static void Reflect(AZ::ReflectContext* context);
+
+            virtual ~PerfMetrics() = default;
+
+            float m_averageDeltaSeconds = 0.0f;
+            float m_timeToFirstRenderSeconds = 0.0f;
+            float m_timingTargetSeconds = 10.0f;
+        };
+
+        AZStd::chrono::time_point<HighResTimer> m_timestamp;
+        float m_accumulatedDeltaSeconds = 0.0f;
+        uint32_t m_frameCount = 0;
+        bool m_testsLogged = false;
+
+        PerfMetrics m_perfMetrics;
+        AZ::Entity* m_atomSampleViewerEntity = nullptr;
+        AZStd::vector<AZ::Name> m_passesToRemove;
+
+        void ReadTimeoutShutdown();
+        void TickTimeoutShutdown(float deltaTimeInSeconds);
+        float m_secondsBeforeShutdown = 0.f; // >0.f If timeout shutdown is enabled, this will count down the time until quit() is called.
+    };
+} // namespace AtomSampleViewer

+ 113 - 0
Gem/Code/Source/Automation/AssetStatusTracker.cpp

@@ -0,0 +1,113 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Automation/AssetStatusTracker.h>
+#include <AzFramework/StringFunc/StringFunc.h>
+
+namespace AtomSampleViewer
+{
+    AssetStatusTracker::~AssetStatusTracker()
+    {
+        AzFramework::AssetSystemInfoBus::Handler::BusDisconnect();
+    }
+
+    void AssetStatusTracker::StartTracking()
+    {
+        if (!m_isTracking)
+        {
+            AzFramework::AssetSystemInfoBus::Handler::BusConnect();
+        }
+
+        m_isTracking = true;
+
+        AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
+        m_allAssetStatusData.clear();
+    }
+
+    void AssetStatusTracker::StopTracking()
+    {
+        if (m_isTracking)
+        {
+            m_isTracking = false;
+
+            AzFramework::AssetSystemInfoBus::Handler::BusDisconnect();
+
+            AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
+            m_allAssetStatusData.clear();
+        }
+    }
+
+    void AssetStatusTracker::ExpectAsset(AZStd::string sourceAssetPath, uint32_t expectedCount)
+    {
+        AzFramework::StringFunc::Path::Normalize(sourceAssetPath);
+        AZStd::to_lower(sourceAssetPath.begin(), sourceAssetPath.end());
+
+        AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
+        m_allAssetStatusData[sourceAssetPath].m_expecteCount += expectedCount;
+    }
+
+    bool AssetStatusTracker::DidExpectedAssetsFinish() const
+    {
+        AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
+
+        for (auto& assetData : m_allAssetStatusData)
+        {
+            const AZStd::string& assetPath = assetData.first;
+            const AssetStatusEvents& status = assetData.second;
+
+            if (status.m_expecteCount > (status.m_succeeded + status.m_failed))
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    void AssetStatusTracker::AssetCompilationStarted(const AZStd::string& assetPath)
+    {
+        AZ_TracePrintf("Automation", "AssetCompilationStarted(%s)\n", assetPath.c_str());
+
+        AZStd::string normalizedAssetPath = assetPath;
+        AzFramework::StringFunc::Path::Normalize(normalizedAssetPath);
+        AZStd::to_lower(normalizedAssetPath.begin(), normalizedAssetPath.end());
+
+        AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
+        m_allAssetStatusData[normalizedAssetPath].m_started++;
+    }
+
+    void AssetStatusTracker::AssetCompilationSuccess(const AZStd::string& assetPath)
+    {
+        AZ_TracePrintf("Automation", "AssetCompilationSuccess(%s)\n", assetPath.c_str());
+
+        AZStd::string normalizedAssetPath = assetPath;
+        AzFramework::StringFunc::Path::Normalize(normalizedAssetPath);
+        AZStd::to_lower(normalizedAssetPath.begin(), normalizedAssetPath.end());
+
+        AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
+        m_allAssetStatusData[normalizedAssetPath].m_succeeded++;
+    }
+
+    void AssetStatusTracker::AssetCompilationFailed(const AZStd::string& assetPath)
+    {
+        AZ_TracePrintf("Automation", "AssetCompilationFailed(%s)\n", assetPath.c_str());
+
+        AZStd::string normalizedAssetPath = assetPath;
+        AzFramework::StringFunc::Path::Normalize(normalizedAssetPath);
+        AZStd::to_lower(normalizedAssetPath.begin(), normalizedAssetPath.end());
+
+        AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
+        m_allAssetStatusData[normalizedAssetPath].m_failed++;
+    }
+
+
+} // namespace AtomSampleViewer

+ 64 - 0
Gem/Code/Source/Automation/AssetStatusTracker.h

@@ -0,0 +1,64 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzFramework/Asset/AssetSystemBus.h>
+
+namespace AtomSampleViewer
+{
+    //! Utility for tracking status of assets being built by the asset processor so scripts can insert delays
+    class AssetStatusTracker final
+       : public AzFramework::AssetSystemInfoBus::Handler
+    {
+    public:
+        ~AssetStatusTracker();
+
+        //! Starts tracking asset status updates from the Asset Processor.
+        //! Clears any asset status information already collected.
+        //! Clears any asset expectations that were added by ExpectAsset().
+        void StartTracking();
+
+        //! Sets the AssetStatusTracker to expect a particular asset with specific expected results.
+        //! Note this can be called multiple times with the same assetPath, in which case the expected counts will be added.
+        //! @param sourceAssetPath the source asset path, relative to the watch folder. Will be normalized and matched case-insensitive.
+        //! @param expectedCount number of completed jobs expected for this asset.
+        void ExpectAsset(AZStd::string sourceAssetPath, uint32_t expectedCount = 1);
+
+        //! Returns whether all of the expected assets have finished.
+        bool DidExpectedAssetsFinish() const;
+
+        //! Stops tracking asset status updates from the Asset Processor. Clears any asset status information already collected.
+        void StopTracking();
+
+    private:
+
+        // Tracks the number of times various events occur
+        struct AssetStatusEvents
+        {
+            uint32_t m_started = 0;
+            uint32_t m_succeeded = 0;
+            uint32_t m_failed = 0;
+            uint32_t m_expecteCount = 0;
+        };
+
+        // AssetSystemInfoBus overrides...
+        void AssetCompilationStarted(const AZStd::string& assetPath) override;
+        void AssetCompilationSuccess(const AZStd::string& assetPath) override;
+        void AssetCompilationFailed(const AZStd::string& assetPath) override;
+
+        bool m_isTracking = false;
+
+        AZStd::unordered_map<AZStd::string /*asset path*/, AssetStatusEvents> m_allAssetStatusData;
+        mutable AZStd::mutex m_mutex;
+    };
+} // namespace AtomSampleViewer

+ 50 - 0
Gem/Code/Source/Automation/ImageComparisonConfig.cpp

@@ -0,0 +1,50 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Automation/ImageComparisonConfig.h>
+#include <AzCore/Serialization/SerializeContext.h>
+
+namespace AtomSampleViewer
+{
+
+    void ImageComparisonConfig::Reflect(AZ::ReflectContext* context)
+    {
+        ImageComparisonToleranceLevel::Reflect(context);
+
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<ImageComparisonConfig>()
+                ->Version(0)
+                ->Field("toleranceLevels", &ImageComparisonConfig::m_toleranceLevels)
+                ;
+        }
+    }
+
+    void ImageComparisonToleranceLevel::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<ImageComparisonToleranceLevel>()
+                ->Version(0)
+                ->Field("name", &ImageComparisonToleranceLevel::m_name)
+                ->Field("threshold", &ImageComparisonToleranceLevel::m_threshold)
+                ->Field("filterImperceptibleDiffs", &ImageComparisonToleranceLevel::m_filterImperceptibleDiffs)
+                ;
+        }
+    }
+
+    AZStd::string ImageComparisonToleranceLevel::ToString() const
+    {
+        return AZStd::string::format("'%s' (threshold %f%s)", m_name.c_str(), m_threshold, m_filterImperceptibleDiffs ? ", filtered" : "");
+    }
+
+} // namespace AtomSampleViewer

+ 50 - 0
Gem/Code/Source/Automation/ImageComparisonConfig.h

@@ -0,0 +1,50 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/std/containers/map.h>
+#include <AzCore/std/string/string.h>
+#include <AzCore/RTTI/RTTI.h>
+
+namespace AZ
+{
+    class ReflectContext;
+}
+
+namespace AtomSampleViewer
+{
+    struct ImageComparisonToleranceLevel
+    {
+        AZ_TYPE_INFO(AtomSampleViewer::ImageComparisonToleranceLevel, "{C9B16AE8-71B4-48D8-A1DF-1128600EDE7A}")
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        AZStd::string m_name;                     //!< A unique name for this tolerance level
+        float m_threshold = 0.0f;                 //!< Range should be 0-1, with 0 meaning no error and 1 meaning as different as possible.
+        bool m_filterImperceptibleDiffs = false;  //!< If true, visually imperceptible differences will be filtered out before scoring.
+
+        AZStd::string ToString() const;
+    };
+
+    struct ImageComparisonConfig final
+    {
+        AZ_TYPE_INFO(AtomSampleViewer::ImageComparisonConfig, "{7D5C0F1E-BEB7-4C80-B1A7-EDBD55EF6119}")
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        //! Lists available tolerance levels sorted from most- to least-strict.
+        AZStd::vector<ImageComparisonToleranceLevel> m_toleranceLevels;
+    };
+
+} // namespace AtomSampleViewer

+ 1550 - 0
Gem/Code/Source/Automation/ScriptManager.cpp

@@ -0,0 +1,1550 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Automation/ScriptManager.h>
+#include <Automation/ScriptableImGui.h>
+#include <SampleComponentManagerBus.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/Component/DebugCamera/CameraComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+#include <Atom/Feature/ImGui/SystemBus.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <AzCore/Component/Entity.h>
+#include <AzCore/Script/ScriptContext.h>
+#include <AzCore/Script/ScriptSystemBus.h>
+#include <AzCore/Script/ScriptAsset.h>
+#include <AzCore/Math/MathReflection.h>
+#include <AzCore/Console/IConsole.h>
+
+#include <AzFramework/API/ApplicationAPI.h>
+#include <AzFramework/Components/ConsoleBus.h>
+#include <AzFramework/IO/LocalFileIO.h>
+#include <AzFramework/Windowing/WindowBus.h>
+
+#include <AtomCore/Serialization/Json/JsonUtils.h>
+#include <AtomSampleViewerRequestBus.h>
+#include <Utils/Utils.h>
+
+namespace AtomSampleViewer
+{
+    ScriptManager* ScriptManager::s_instance = nullptr;
+
+    ScriptManager::ScriptManager()
+        : m_scriptBrowser("@user@/lua_script_browser.xml")
+    {
+    }
+
+    void ScriptManager::Activate()
+    {
+        AZ_Assert(s_instance == nullptr, "ScriptManager is already activated");
+        s_instance = this;
+
+        ScriptableImGui::Create();
+
+        m_scriptContext = AZStd::make_unique<AZ::ScriptContext>();
+        m_sriptBehaviorContext = AZStd::make_unique<AZ::BehaviorContext>();
+        ReflectScriptContext(m_sriptBehaviorContext.get());
+        m_scriptContext->BindTo(m_sriptBehaviorContext.get());
+
+        m_scriptBrowser.SetFilter([this](const AZ::Data::AssetInfo& assetInfo)
+        {
+            return AzFramework::StringFunc::EndsWith(assetInfo.m_relativePath, ".bv.luac");
+        });
+
+        m_scriptBrowser.Activate();
+
+        ScriptRepeaterRequestBus::Handler::BusConnect();
+        ScriptRunnerRequestBus::Handler::BusConnect();
+
+        m_imageComparisonOptions.Activate();
+    }
+
+    void ScriptManager::Deactivate()
+    {
+        s_instance = nullptr;
+        m_scriptContext = nullptr;
+        m_sriptBehaviorContext = nullptr;
+        m_scriptBrowser.Deactivate();
+        ScriptableImGui::Destory();
+        m_imageComparisonOptions.Deactivate();
+        ScriptRunnerRequestBus::Handler::BusDisconnect();
+        ScriptRepeaterRequestBus::Handler::BusDisconnect();
+        AZ::Debug::CameraControllerNotificationBus::Handler::BusDisconnect();
+    }
+
+    void ScriptManager::SetCameraEntity(AZ::Entity* cameraEntity)
+    {
+        AZ::Debug::CameraControllerNotificationBus::Handler::BusDisconnect();
+        m_cameraEntity = cameraEntity;
+        AZ::Debug::CameraControllerNotificationBus::Handler::BusConnect(m_cameraEntity->GetId());
+    }
+
+    void ScriptManager::PauseScript()
+    {
+        PauseScriptWithTimeout(DefaultPauseTimeout);
+    }
+
+    void ScriptManager::PauseScriptWithTimeout(float timeout)
+    {
+        m_scriptPaused = true;
+        m_scriptPauseTimeout = AZ::GetMax(timeout, m_scriptPauseTimeout);
+    }
+
+    void ScriptManager::ResumeScript()
+    {
+        AZ_Warning("Automation", m_scriptPaused, "Script is not paused");
+        m_scriptPaused = false;
+    }
+
+    void ScriptManager::ReportScriptError([[maybe_unused]] const AZStd::string& message)
+    {
+        AZ_Error("Automation", false, "Script: %s", message.c_str());
+    }
+
+    void ScriptManager::ReportScriptWarning([[maybe_unused]] const AZStd::string& message)
+    {
+        AZ_Warning("Automation", false, "Script: %s", message.c_str());
+    }
+
+    void ScriptManager::ReportScriptableAction([[maybe_unused]] AZStd::string_view scriptCommand)
+    {
+        AZ_TracePrintf("Automation", "Scriptable Action: %.*s\n", AZ_STRING_ARG(scriptCommand));
+    }
+
+
+    void ScriptManager::ImageComparisonOptions::Activate()
+    {
+        m_configAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath<AZ::RPI::AnyAsset>("config/ImageComparisonConfig.azasset", AZ::RPI::AssetUtils::TraceLevel::Assert);
+        if (m_configAsset)
+        {
+            AZ::Data::AssetBus::Handler::BusConnect(m_configAsset.GetId());
+            OnAssetReloaded(m_configAsset);
+        }
+    }
+
+    void ScriptManager::ImageComparisonOptions::Deactivate()
+    {
+        m_configAsset.Release();
+        ResetImGuiSettings();
+        AZ::Data::AssetBus::Handler::BusDisconnect();
+    }
+
+    ImageComparisonToleranceLevel* ScriptManager::ImageComparisonOptions::FindToleranceLevel(const AZStd::string& name, bool allowLevelAdjustment)
+    {
+        size_t foundIndex = m_config.m_toleranceLevels.size();
+
+        for (size_t i = 0; i < m_config.m_toleranceLevels.size(); ++i)
+        {
+            if (m_config.m_toleranceLevels[i].m_name == name)
+            {
+                foundIndex = i;
+                break;
+            }
+        }
+
+        if (foundIndex == m_config.m_toleranceLevels.size())
+        {
+            return nullptr;
+        }
+
+        if (allowLevelAdjustment)
+        {
+            int adjustedIndex = aznumeric_cast<int>(foundIndex);
+            adjustedIndex += m_toleranceAdjustment;
+            adjustedIndex = AZ::GetClamp(adjustedIndex, 0, aznumeric_cast<int>(m_config.m_toleranceLevels.size()) - 1);
+            foundIndex = aznumeric_cast<size_t>(adjustedIndex);
+        }
+
+        return &m_config.m_toleranceLevels[foundIndex];
+    }
+
+    const AZStd::vector<ImageComparisonToleranceLevel>& ScriptManager::ImageComparisonOptions::GetAvailableToleranceLevels() const
+    {
+        return m_config.m_toleranceLevels;
+    }
+
+    void ScriptManager::ImageComparisonOptions::SelectToleranceLevel(const AZStd::string& name, bool allowLevelAdjustment)
+    {
+        if (m_selectedOverrideSetting == OverrideSetting_ScriptControlled)
+        {
+            ImageComparisonToleranceLevel* level = FindToleranceLevel(name, allowLevelAdjustment);
+
+            if (level)
+            {
+                m_currentToleranceLevel = level;
+            }
+            else
+            {
+                ReportScriptError(AZStd::string::format("ImageComparisonToleranceLevel '%s' not found.", name.c_str()));
+            }
+        }
+    }
+
+    void ScriptManager::ImageComparisonOptions::SelectToleranceLevel(ImageComparisonToleranceLevel* level)
+    {
+        if (nullptr == level)
+        {
+            m_currentToleranceLevel = level;
+            return;
+        }
+        else
+        {
+            SelectToleranceLevel(level->m_name);
+            AZ_Assert(GetCurrentToleranceLevel() == level, "Wrong ImageComparisonToleranceLevel pointer used");
+        }
+    }
+
+    ImageComparisonToleranceLevel* ScriptManager::ImageComparisonOptions::GetCurrentToleranceLevel()
+    {
+        return m_currentToleranceLevel;
+    }
+
+    bool ScriptManager::ImageComparisonOptions::IsScriptControlled() const
+    {
+        return m_selectedOverrideSetting == OverrideSetting_ScriptControlled;
+    }
+
+    bool ScriptManager::ImageComparisonOptions::IsLevelAdjusted() const
+    {
+        return m_toleranceAdjustment != 0;
+    }
+
+    void ScriptManager::ImageComparisonOptions::DrawImGuiSettings()
+    {
+        ImGui::Text("Tolerance");
+        ImGui::Indent();
+
+        if (ImGui::Combo("Level",
+            &m_selectedOverrideSetting,
+            m_overrideSettings.data(),
+            aznumeric_cast<int>(m_overrideSettings.size())))
+        {
+            if (m_selectedOverrideSetting == OverrideSetting_ScriptControlled)
+            {
+                m_currentToleranceLevel = nullptr;
+            }
+            else
+            {
+                m_currentToleranceLevel = &m_config.m_toleranceLevels[m_selectedOverrideSetting - 1];
+            }
+        }
+
+        if (IsScriptControlled())
+        {
+            ImGui::InputInt("Level Adjustment", &m_toleranceAdjustment);
+        }
+
+        ImGui::Unindent();
+    }
+
+    void ScriptManager::ImageComparisonOptions::ResetImGuiSettings()
+    {
+        m_currentToleranceLevel = nullptr;
+        m_selectedOverrideSetting = OverrideSetting_ScriptControlled;
+        m_toleranceAdjustment = 0;
+    }
+
+    void ScriptManager::ImageComparisonOptions::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
+    {
+        m_configAsset = asset;
+        m_config = *m_configAsset->GetDataAs<ImageComparisonConfig>();
+
+        m_overrideSettings.clear();
+        m_overrideSettings.push_back("[Script-controlled]");
+        for (size_t i = 0; i < m_config.m_toleranceLevels.size(); ++i)
+        {
+            AZ_Assert(i == 0 || m_config.m_toleranceLevels[i].m_threshold > m_config.m_toleranceLevels[i - 1].m_threshold, "Threshold values are not sequential");
+
+            m_overrideSettings.push_back(m_config.m_toleranceLevels[i].m_name.c_str());
+        }
+    }
+
+    void ScriptManager::TickImGui()
+    {
+        if (m_showScriptRunnerDialog)
+        {
+            ShowScriptRunnerDialog();
+        }
+
+        m_scriptReporter.TickImGui();
+
+        if (m_testSuiteRunConfig.m_automatedRunEnabled)
+        {
+            if (m_testSuiteRunConfig.m_isStarted == false)
+            {
+                m_testSuiteRunConfig.m_isStarted = true;
+                PrepareAndExecuteScript(m_testSuiteRunConfig.m_testSuitePath);
+            }
+        }
+    }
+
+    void ScriptManager::TickScript(float deltaTime)
+    {
+        // All actions must be consumed each frame. Otherwise, this indicates that a script is
+        // scheduling ScriptableImGui actions for fields that don't exist.
+        ScriptableImGui::CheckAllActionsConsumed();
+        ScriptableImGui::ClearActions();
+
+        // We delayed PopScript() until after the above CheckAllActionsConsumed(), so that any errors
+        // reported by that function will be associated with the proper script.
+        if (m_shouldPopScript)
+        {
+            m_scriptReporter.PopScript();
+            m_shouldPopScript = false;
+        }
+
+        while (!m_scriptOperations.empty())
+        {
+            if (m_shouldPopScript)
+            {
+                // If we just finished executing a script, the remaining m_scriptOperations are for some other script.
+                // We need to proceed to the next frame and allow that PopScript() to happen, otherwise any errors related
+                // to subsequent operations would be reported against the prior script.
+                break;
+            }
+
+            if (m_scriptPaused)
+            {
+                m_scriptPauseTimeout -= deltaTime;
+                if (m_scriptPauseTimeout < 0)
+                {
+                    AZ_Error("Automation", false, "Script pause timed out. Continuing...");
+                    m_scriptPaused = false;
+                }
+                else
+                {
+                    break;
+                }
+            }
+
+            if (m_waitForAssetTracker)
+            {
+                m_assetTrackingTimeout -= deltaTime;
+                if (m_assetTrackingTimeout < 0)
+                {
+                    AZ_Error("Automation", false, "Script asset tracking timed out. Continuing...");
+                    m_waitForAssetTracker = false;
+                }
+                else if (m_assetStatusTracker.DidExpectedAssetsFinish())
+                {
+                    m_waitForAssetTracker = false;
+                }
+                else
+                {
+                    break;
+                }
+            }
+
+            if (m_scriptIdleFrames > 0)
+            {
+                m_scriptIdleFrames--;
+                break;
+            }
+
+            if (m_scriptIdleSeconds > 0)
+            {
+                m_scriptIdleSeconds -= deltaTime;
+                break;
+            }
+
+            // Execute the next operation
+            m_scriptOperations.front()();
+
+            m_scriptOperations.pop();
+
+            if (m_scriptOperations.empty())
+            {
+                m_doFinalScriptCleanup = true;
+            }
+        }
+
+        if (m_shouldPopScript)
+        {
+            // We need to proceed for one more frame to do the last PopScript() before final cleanup
+            return;
+        }
+
+        if (m_doFinalScriptCleanup)
+        {
+            bool frameCapturePending = false;
+            SampleComponentManagerRequestBus::BroadcastResult(frameCapturePending, &SampleComponentManagerRequests::IsFrameCapturePending);
+            if (!frameCapturePending && !m_isCapturePending)
+            {
+                AZ_Assert(m_scriptPaused == false, "Script manager is in an unexpected state.");
+                AZ_Assert(m_scriptIdleFrames == 0, "Script manager is in an unexpected state.");
+                AZ_Assert(m_scriptIdleSeconds <= 0.0f, "Script manager is in an unexpected state.");
+                AZ_Assert(m_waitForAssetTracker == false, "Script manager is in an unexpected state.");
+                AZ_Assert(!m_scriptReporter.HasActiveScript(), "Script manager is in an unexpected state.");
+                AZ_Assert(m_executingScripts.size() == 0, "Script manager is in an unexpected state");
+
+                m_assetStatusTracker.StopTracking();
+
+                if (m_frameTimeIsLocked)
+                {
+                    AZ::Interface<AZ::IConsole>::Get()->PerformCommand("t_frameTimeOverride 0");
+                    m_frameTimeIsLocked = false;
+                }
+
+                if (m_shouldRestoreViewportSize)
+                {
+                    Utils::ResizeClientArea(m_savedViewportWidth, m_savedViewportHeight);
+                    m_shouldRestoreViewportSize = false;
+                }
+
+                // In case scripts were aborted while ImGui was temporarily hidden, show it again.
+                SetShowImGui(true);
+
+                m_scriptReporter.OpenReportDialog();
+
+                m_shouldPopScript = false;
+                m_doFinalScriptCleanup = false;
+
+                if (m_testSuiteRunConfig.m_automatedRunEnabled && m_testSuiteRunConfig.m_closeOnTestScriptFinish)
+                {
+                    m_testSuiteRunConfig.m_automatedRunEnabled = false;
+
+                    if (m_scriptReporter.HasErrorsAssertsInReport())
+                    {
+                        AtomSampleViewerRequestsBus::Broadcast(&AtomSampleViewerRequestsBus::Events::SetExitCode, 1);
+
+                        // Useful console logging for Hydra tests
+                        int failedTests = 0;
+                        for (const ScriptReporter::ScriptReport& testReport : m_scriptReporter.GetScriptReport())
+                        {
+                            if (testReport.m_assertCount > 0 || testReport.m_generalErrorCount > 0 || testReport.m_screenshotErrorCount > 0)
+                            {
+                                ++failedTests;
+
+                                AZ_Printf("AtomSampleViewer", "Test failure %s: asserts %u, general errors %u, screenshot failures %u\n", testReport.m_scriptAssetPath.c_str(),
+                                    testReport.m_assertCount, testReport.m_generalErrorCount, testReport.m_screenshotErrorCount);
+                            }
+                        }
+
+                        AZ_Printf("AtomSampleViewer", "%d tests failed\n", failedTests);
+                    }
+
+                    AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
+                }
+            }
+        }
+    }
+
+    void ScriptManager::OpenScriptRunnerDialog()
+    {
+        m_showScriptRunnerDialog = true;
+    }
+
+    void ScriptManager::RunMainTestSuite(const AZStd::string& suiteFilePath, bool exitOnTestEnd)
+    {
+        m_testSuiteRunConfig.m_automatedRunEnabled = true;
+        m_testSuiteRunConfig.m_testSuitePath = suiteFilePath;
+        m_testSuiteRunConfig.m_closeOnTestScriptFinish = exitOnTestEnd;
+    }
+
+    void ScriptManager::AbortScripts(const AZStd::string& reason)
+    {
+        m_scriptReporter.SetInvalidationMessage(reason);
+
+        m_scriptOperations = {};
+        m_executingScripts.clear();
+        m_scriptPaused = false;
+        m_scriptIdleFrames = 0;
+        m_scriptIdleSeconds = 0.0f;
+        m_waitForAssetTracker = false;
+        while (m_scriptReporter.HasActiveScript())
+        {
+            m_scriptReporter.PopScript();
+        }
+
+        m_doFinalScriptCleanup = true;
+    }
+
+    void ScriptManager::ShowScriptRunnerDialog()
+    {
+        if (ImGui::Begin("Script Runner", &m_showScriptRunnerDialog))
+        {
+            auto drawAbortButton = [this](const char* uniqueId)
+            {
+                ImGui::PushID(uniqueId);
+
+                if (ImGui::Button("Abort"))
+                {
+                    AbortScripts("Script(s) manually aborted.");
+                }
+
+                ImGui::PopID();
+            };
+
+            // The main buttons are at the bottom, but show the Abort button at the top too, in case the window size is small.
+            if (!m_scriptOperations.empty())
+            {
+                drawAbortButton("Button1");
+            }
+
+            ImGuiAssetBrowser::WidgetSettings assetBrowserSettings;
+            assetBrowserSettings.m_labels.m_root = "Lua Scripts";
+            m_scriptBrowser.Tick(assetBrowserSettings);
+
+            AZStd::string selectedFileName = "<none>";
+            AzFramework::StringFunc::Path::GetFullFileName(m_scriptBrowser.GetSelectedAssetPath().c_str(), selectedFileName);
+            ImGui::LabelText("##SelectedScript", "Selected: %s", selectedFileName.c_str());
+
+            ImGui::Separator();
+
+            ImGui::Text("Settings");
+            ImGui::Indent();
+
+            m_imageComparisonOptions.DrawImGuiSettings();
+            if (ImGui::Button("Reset"))
+            {
+                m_imageComparisonOptions.ResetImGuiSettings();
+            }
+
+            ImGui::Unindent();
+
+            ImGui::Separator();
+
+            if (ImGui::Button("Run"))
+            {
+                auto scriptAsset = m_scriptBrowser.GetSelectedAsset<AZ::ScriptAsset>();
+                if (scriptAsset.GetId().IsValid())
+                {
+                    PrepareAndExecuteScript(m_scriptBrowser.GetSelectedAssetPath());
+                }
+            }
+
+            if (ImGui::Button("View Latest Results"))
+            {
+                m_scriptReporter.OpenReportDialog();
+            }
+
+            if (m_scriptOperations.size() > 0)
+            {
+                ImGui::LabelText("##RunningScript", "Running %zu operations...", m_scriptOperations.size());
+
+                drawAbortButton("Button2");
+            }
+        }
+
+        m_messageBox.TickPopup();
+
+        ImGui::End();
+    }
+
+    void ScriptManager::PrepareAndExecuteScript(const AZStd::string& scriptFilePath)
+    {
+        ReportScriptableAction(AZStd::string::format("RunScript('%s')", scriptFilePath.c_str()));
+
+        // Save the window size so we can restore it after running the script, in case the script calls ResizeViewport
+        AzFramework::NativeWindowHandle defaultWindowHandle;
+        AzFramework::WindowSize windowSize;
+        AzFramework::WindowSystemRequestBus::BroadcastResult(defaultWindowHandle, &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle);
+        AzFramework::WindowRequestBus::EventResult(windowSize, defaultWindowHandle, &AzFramework::WindowRequests::GetClientAreaSize);
+        m_savedViewportWidth = windowSize.m_width;
+        m_savedViewportHeight = windowSize.m_height;
+        if (m_savedViewportWidth == 0 || m_savedViewportHeight == 0)
+        {
+            AZ_Assert(false, "Could not get current window size");
+        }
+        else
+        {
+            m_shouldRestoreViewportSize = true;
+        }
+
+        // Setup the ScriptReporter to track and report the results
+        m_scriptReporter.Reset();
+        m_scriptReporter.SetAvailableToleranceLevels(m_imageComparisonOptions.GetAvailableToleranceLevels());
+        if (m_imageComparisonOptions.IsLevelAdjusted())
+        {
+            m_scriptReporter.SetInvalidationMessage("Results are invalid because the tolerance level has been adjusted.");
+        }
+        else if (!m_imageComparisonOptions.IsScriptControlled())
+        {
+            m_scriptReporter.SetInvalidationMessage("Results are invalid because the tolerance level has been overridden.");
+        }
+        else
+        {
+            m_scriptReporter.SetInvalidationMessage("");
+        }
+
+        AZ_Assert(m_executingScripts.empty(), "There should be no active scripts at this point");
+
+        ExecuteScript(scriptFilePath);
+    }
+
+    void ScriptManager::ExecuteScript(const AZStd::string& scriptFilePath)
+    {
+        AZ::Data::Asset<AZ::ScriptAsset> scriptAsset = AZ::RPI::AssetUtils::LoadAssetByProductPath<AZ::ScriptAsset>(scriptFilePath.c_str());
+        if (!scriptAsset)
+        {
+            // Push an error operation on the back of the queue instead of reporting it immediately so it doesn't get lost
+            // in front of a bunch of queued m_scriptOperations.
+            Script_Error(AZStd::string::format("Could not find or load script asset '%s'.", scriptFilePath.c_str()));
+            return;
+        }
+
+        if (s_instance->m_executingScripts.find(scriptAsset.GetId()) != s_instance->m_executingScripts.end())
+        {
+            Script_Error(AZStd::string::format("Calling script '%s' would likely cause an infinite loop and crash. Skipping.", scriptFilePath.c_str()));
+            return;
+        }
+
+        if (s_instance->m_imageComparisonOptions.IsScriptControlled())
+        {
+            s_instance->m_imageComparisonOptions.SelectToleranceLevel(nullptr); // Clear the preset before each script to make sure the script is selecting it.
+        }
+
+        // Execute(script) will add commands to the m_scriptOperations. These should be considered part of their own test script, for reporting purposes.
+        s_instance->m_scriptOperations.push([scriptFilePath]()
+            {
+                s_instance->m_scriptReporter.PushScript(scriptFilePath);
+            }
+        );
+
+        s_instance->m_scriptOperations.push([scriptFilePath]()
+            {
+                AZ_Printf("Automation", "Running script '%s'...\n", scriptFilePath.c_str());
+            }
+        );
+
+        s_instance->m_executingScripts.insert(scriptAsset.GetId());
+
+        if (!s_instance->m_scriptContext->Execute(scriptAsset->GetScriptBuffer().data(), scriptFilePath.c_str(), scriptAsset->GetScriptBuffer().size()))
+        {
+            // Push an error operation on the back of the queue instead of reporting it immediately so it doesn't get lost
+            // in front of a bunch of queued m_scriptOperations.
+            Script_Error(AZStd::string::format("Error running script '%s'.", scriptAsset.ToString<AZStd::string>().c_str()));
+        }
+
+        s_instance->m_executingScripts.erase(scriptAsset.GetId());
+
+        // Execute(script) will have added commands to the m_scriptOperations. When they finish, consider this test as completed, for reporting purposes.
+        s_instance->m_scriptOperations.push([]()
+            {
+                // We don't call m_scriptReporter.PopScript() yet because some cleanup needs to happen in TickScript() on the next frame.
+                AZ_Assert(!s_instance->m_shouldPopScript, "m_shouldPopScript is already true");
+                s_instance->m_shouldPopScript = true;
+            }
+        );
+    }
+
+    void ScriptManager::OnCameraMoveEnded(AZ::TypeId controllerTypeId, uint32_t channels)
+    {
+        if (controllerTypeId == azrtti_typeid<AZ::Debug::ArcBallControllerComponent>())
+        {
+            if (channels & AZ::Debug::ArcBallControllerChannel_Center)
+            {
+                AZ::Vector3 center = AZ::Vector3::CreateZero();
+                AZ::Debug::ArcBallControllerRequestBus::EventResult(center, m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequests::GetCenter);
+                ReportScriptableAction(AZStd::string::format("ArcBallCameraController_SetCenter(Vector3(%f, %f, %f))", (float)center.GetX(), (float)center.GetY(), (float)center.GetZ()));
+            }
+
+            if (channels & AZ::Debug::ArcBallControllerChannel_Pan)
+            {
+                AZ::Vector3 pan = AZ::Vector3::CreateZero();
+                AZ::Debug::ArcBallControllerRequestBus::EventResult(pan, m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequests::GetPan);
+                ReportScriptableAction(AZStd::string::format("ArcBallCameraController_SetPan(Vector3(%f, %f, %f))", (float)pan.GetX(), (float)pan.GetY(), (float)pan.GetZ()));
+            }
+
+            if (channels & AZ::Debug::ArcBallControllerChannel_Heading)
+            {
+                float heading = 0.0;
+                AZ::Debug::ArcBallControllerRequestBus::EventResult(heading, m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequests::GetHeading);
+                ReportScriptableAction(AZStd::string::format("ArcBallCameraController_SetHeading(DegToRad(%f))", AZ::RadToDeg(heading)));
+            }
+
+            if (channels & AZ::Debug::ArcBallControllerChannel_Pitch)
+            {
+                float pitch = 0.0;
+                AZ::Debug::ArcBallControllerRequestBus::EventResult(pitch, m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequests::GetPitch);
+                ReportScriptableAction(AZStd::string::format("ArcBallCameraController_SetPitch(DegToRad(%f))", AZ::RadToDeg(pitch)));
+            }
+
+            if (channels & AZ::Debug::ArcBallControllerChannel_Distance)
+            {
+                float distance = 0.0;
+                AZ::Debug::ArcBallControllerRequestBus::EventResult(distance, m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequests::GetDistance);
+                ReportScriptableAction(AZStd::string::format("ArcBallCameraController_SetDistance(%f)", distance));
+            }
+        }
+
+        if (controllerTypeId == azrtti_typeid<AZ::Debug::NoClipControllerComponent>())
+        {
+            if (channels & AZ::Debug::NoClipControllerChannel_Position)
+            {
+                AZ::Vector3 position = AZ::Vector3::CreateZero();
+                AZ::Debug::NoClipControllerRequestBus::EventResult(position, m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequests::GetPosition);
+                ReportScriptableAction(AZStd::string::format("NoClipCameraController_SetPosition(Vector3(%f, %f, %f))", (float)position.GetX(), (float)position.GetY(), (float)position.GetZ()));
+            }
+
+            if (channels & AZ::Debug::NoClipControllerChannel_Orientation)
+            {
+                float heading = 0.0;
+                AZ::Debug::NoClipControllerRequestBus::EventResult(heading, m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequests::GetHeading);
+                ReportScriptableAction(AZStd::string::format("NoClipCameraController_SetHeading(DegToRad(%f))", AZ::RadToDeg(heading)));
+
+                float pitch = 0.0;
+                AZ::Debug::NoClipControllerRequestBus::EventResult(pitch, m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequests::GetPitch);
+                ReportScriptableAction(AZStd::string::format("NoClipCameraController_SetPitch(DegToRad(%f))", AZ::RadToDeg(pitch)));
+            }
+
+            if (channels & AZ::Debug::NoClipControllerChannel_Fov)
+            {
+                float fov = 0.0;
+                AZ::Debug::NoClipControllerRequestBus::EventResult(fov, m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequests::GetFov);
+                ReportScriptableAction(AZStd::string::format("NoClipCameraController_SetFov(DegToRad(%f))", AZ::RadToDeg(fov)));
+            }
+        }
+    }
+
+    void ScriptManager::ReflectScriptContext(AZ::BehaviorContext* behaviorContext)
+    {
+        AZ::MathReflect(behaviorContext);
+
+        // Utilities...
+        behaviorContext->Method("RunScript", &Script_RunScript);
+        behaviorContext->Method("Error", &Script_Error);
+        behaviorContext->Method("Warning", &Script_Warning);
+        behaviorContext->Method("Print", &Script_Print);
+        behaviorContext->Method("IdleFrames", &Script_IdleFrames);
+        behaviorContext->Method("IdleSeconds", &Script_IdleSeconds);
+        behaviorContext->Method("LockFrameTime", &Script_LockFrameTime);
+        behaviorContext->Method("UnlockFrameTime", &Script_UnlockFrameTime);
+        behaviorContext->Method("ResizeViewport", &Script_ResizeViewport);
+        behaviorContext->Method("SetShowImGui", &Script_SetShowImGui);
+        behaviorContext->Method("ExecuteConsoleCommand", &Script_ExecuteConsoleCommand);
+
+        // Utilities returning data (these special functions do return data because they don't read dynamic state)...
+        behaviorContext->Method("ResolvePath", &Script_ResolvePath);
+        behaviorContext->Method("NormalizePath", &Script_NormalizePath);
+        behaviorContext->Method("DegToRad", &Script_DegToRad);
+        behaviorContext->Method("GetRenderApiName", &Script_GetRenderApiName);
+
+        // Samples...
+        behaviorContext->Method("OpenSample", &Script_OpenSample);
+        behaviorContext->Method("SetImguiValue", &Script_SetImguiValue);
+
+        // Debug profilers...
+        behaviorContext->Method("ShowTool", &Script_ShowTool);
+
+        // Screenshots...
+        behaviorContext->Method("SelectImageComparisonToleranceLevel", &Script_SelectImageComparisonToleranceLevel);
+        behaviorContext->Method("CaptureScreenshot", &Script_CaptureScreenshot);
+        behaviorContext->Method("CaptureScreenshotWithImGui", &Script_CaptureScreenshotWithImGui);
+        behaviorContext->Method("CaptureScreenshotWithPreview", &Script_CaptureScreenshotWithPreview);
+        behaviorContext->Method("CapturePassAttachment", &Script_CapturePassAttachment);
+
+        // Profiling data...
+        behaviorContext->Method("CapturePassTimestamp", &Script_CapturePassTimestamp);
+        behaviorContext->Method("CapturePassPipelineStatistics", &Script_CapturePassPipelineStatistics);
+        behaviorContext->Method("CaptureCpuProfilingStatistics", &Script_CaptureCpuProfilingStatistics);
+
+        // Camera...
+        behaviorContext->Method("ArcBallCameraController_SetCenter", &Script_ArcBallCameraController_SetCenter);
+        behaviorContext->Method("ArcBallCameraController_SetPan", &Script_ArcBallCameraController_SetPan);
+        behaviorContext->Method("ArcBallCameraController_SetDistance", &Script_ArcBallCameraController_SetDistance);
+        behaviorContext->Method("ArcBallCameraController_SetHeading", &Script_ArcBallCameraController_SetHeading);
+        behaviorContext->Method("ArcBallCameraController_SetPitch", &Script_ArcBallCameraController_SetPitch);
+        behaviorContext->Method("NoClipCameraController_SetPosition", &Script_NoClipCameraController_SetPosition);
+        behaviorContext->Method("NoClipCameraController_SetHeading", &Script_NoClipCameraController_SetHeading);
+        behaviorContext->Method("NoClipCameraController_SetPitch", &Script_NoClipCameraController_SetPitch);
+        behaviorContext->Method("NoClipCameraController_SetFov", &Script_NoClipCameraController_SetFov);
+
+        // Asset System...
+        AZ::BehaviorParameterOverrides expectedCountDetails = {"expectedCount", "Expected number of asset jobs; default=1", aznew AZ::BehaviorDefaultValue(1u)};
+        const AZStd::array<AZ::BehaviorParameterOverrides, 2> assetTrackingExpectAssetArgs = {{ AZ::BehaviorParameterOverrides{}, expectedCountDetails }};
+
+        behaviorContext->Method("AssetTracking_Start", &Script_AssetTracking_Start);
+        behaviorContext->Method("AssetTracking_ExpectAsset", &Script_AssetTracking_ExpectAsset, assetTrackingExpectAssetArgs);
+        behaviorContext->Method("AssetTracking_IdleUntilExpectedAssetsFinish", &Script_AssetTracking_IdleUntilExpectedAssetsFinish);
+        behaviorContext->Method("AssetTracking_Stop", &Script_AssetTracking_Stop);
+    }
+
+    void ScriptManager::Script_Error(const AZStd::string& message)
+    {
+        auto func = [message]()
+        {
+            ReportScriptError(message.c_str());
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(func));
+    }
+
+    void ScriptManager::Script_Warning(const AZStd::string& message)
+    {
+        auto func = [message]()
+        {
+            ReportScriptWarning(message.c_str());
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(func));
+    }
+
+    void ScriptManager::Script_Print(const AZStd::string& message)
+    {
+        auto func = [message]()
+        {
+            AZ_TracePrintf("Automation", "Script: %s\n", message.c_str());
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(func));
+    }
+
+    AZStd::string ScriptManager::Script_ResolvePath(const AZStd::string& path)
+    {
+        return Utils::ResolvePath(path);
+    }
+
+    AZStd::string ScriptManager::Script_NormalizePath(const AZStd::string& path)
+    {
+        AZStd::string normalizedPath = path;
+        AzFramework::StringFunc::Path::Normalize(normalizedPath);
+        return normalizedPath;
+    }
+
+    void ScriptManager::Script_OpenSample(const AZStd::string& sampleName)
+    {
+        auto operation = [sampleName]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+            if (sampleName.empty())
+            {
+                SampleComponentManagerRequestBus::Broadcast(&SampleComponentManagerRequests::Reset);
+            }
+            else
+            {
+                bool foundSample = false;
+                SampleComponentManagerRequestBus::BroadcastResult(foundSample, &SampleComponentManagerRequests::OpenSample, sampleName);
+
+                if (foundSample)
+                {
+                    // Samples need a few frames to initialize before consuming actions from ScriptableImGui,
+                    // so we need to wait before letting the script schedule ScriptableImGui actions.
+                    // They need 1 frame to activate, 1 frame to start ticking, and 1 frame to guarantee
+                    // that a sample OnTick occurs before a ScriptManager::OnTick. We schedule
+                    // a few extra just in case.
+                    AZ_Assert(s_instance->m_scriptIdleFrames == 0, "m_scriptIdleFrames is being stomped");
+                    s_instance->m_scriptIdleFrames = 6;
+                }
+            }
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_ShowTool(const AZStd::string& toolName, bool enable)
+    {
+        auto operation = [toolName, enable]()
+        {
+            bool foundTool = false;
+            SampleComponentManagerRequestBus::BroadcastResult(foundTool, &SampleComponentManagerRequests::ShowTool, toolName, enable);
+
+            AZ_Warning("ScriptManager", foundTool, "Can't find [%s] tool", toolName.c_str());
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_RunScript(const AZStd::string& scriptFilePath)
+    {
+        // Unlike other Script_ callback functions, we process immediately instead of pushing onto the m_scriptOperations queue.
+        // This function is special because running the script is what adds more commands onto the m_scriptOperations queue.
+        s_instance->ExecuteScript(scriptFilePath);
+    }
+
+    void ScriptManager::Script_IdleFrames(int numFrames)
+    {
+        auto operation = [numFrames]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+            AZ_Assert(s_instance->m_scriptIdleFrames == 0, "m_scriptIdleFrames is being stomped");
+            s_instance->m_scriptIdleFrames = numFrames;
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_IdleSeconds(float numSeconds)
+    {
+        auto operation = [numSeconds]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+            s_instance->m_scriptIdleSeconds = numSeconds;
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_LockFrameTime(float seconds)
+    {
+        auto operation = [seconds]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+            AZ::Interface<AZ::IConsole>::Get()->PerformCommand(AZStd::string::format("t_frameTimeOverride %f", seconds).c_str());
+            s_instance->m_frameTimeIsLocked = true;
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_UnlockFrameTime()
+    {
+        auto operation = []()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+            AZ::Interface<AZ::IConsole>::Get()->PerformCommand("t_frameTimeOverride 0");
+            s_instance->m_frameTimeIsLocked = false;
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_SetImguiValue(AZ::ScriptDataContext& dc)
+    {
+        if (dc.GetNumArguments() != 2)
+        {
+            ReportScriptError("Wrong number of arguments for SetImguiValue");
+            return;
+        }
+
+        if (!dc.IsString(0))
+        {
+            ReportScriptError("SetImguiValue first argument must be a string");
+            return;
+        }
+
+        const char* fieldName = nullptr;
+        dc.ReadArg(0, fieldName);
+
+        AZStd::string fieldNameString = fieldName; // Because the lambda will need to capture a copy of something, not a pointer
+
+        if (dc.IsBoolean(1))
+        {
+            bool value = false;
+            dc.ReadArg(1, value);
+
+            auto func = [fieldNameString, value]()
+            {
+                ScriptableImGui::SetBool(fieldNameString, value);
+            };
+
+            s_instance->m_scriptOperations.push(AZStd::move(func));
+        }
+        else if (dc.IsNumber(1))
+        {
+            float value = 0.0f;
+            dc.ReadArg(1, value);
+
+            auto func = [fieldNameString, value]()
+            {
+                ScriptableImGui::SetNumber(fieldNameString, value);
+            };
+
+            s_instance->m_scriptOperations.push(AZStd::move(func));
+        }
+        else if (dc.IsString(1))
+        {
+            const char* value = nullptr;
+            dc.ReadArg(1, value);
+
+            AZStd::string valueString = value; // Because the lambda will need to capture a copy of something, not a pointer
+
+            auto func = [fieldNameString, valueString]()
+            {
+                ScriptableImGui::SetString(fieldNameString, valueString);
+            };
+
+            s_instance->m_scriptOperations.push(AZStd::move(func));
+        }
+        else if (dc.IsClass<AZ::Vector3>(1))
+        {
+            AZ::Vector3 value = AZ::Vector3::CreateZero();
+            dc.ReadArg(1, value);
+
+            auto func = [fieldNameString, value]()
+            {
+                ScriptableImGui::SetVector(fieldNameString, value);
+            };
+
+            s_instance->m_scriptOperations.push(AZStd::move(func));
+        }
+        else if (dc.IsClass<AZ::Vector2>(1))
+        {
+            AZ::Vector2 value = AZ::Vector2::CreateZero();
+            dc.ReadArg(1, value);
+
+            auto func = [fieldNameString, value]()
+            {
+                ScriptableImGui::SetVector(fieldNameString, value);
+            };
+
+            s_instance->m_scriptOperations.push(AZStd::move(func));
+        }
+    }
+
+    void ScriptManager::Script_ResizeViewport(int width, int height)
+    {
+        auto operation = [width,height]()
+        {
+            if (Utils::SupportsResizeClientArea())
+            {
+                Utils::ResizeClientArea(width, height);
+            }
+            else
+            {
+                s_instance->ReportScriptError("ResizeViewport() is not supported on this platform");
+            }
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_ExecuteConsoleCommand(const AZStd::string& command)
+    {
+        auto operation = [command]()
+        {
+            AzFramework::ConsoleRequestBus::Broadcast(&AzFramework::ConsoleRequests::ExecuteConsoleCommand, command.c_str());
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::SetShowImGui(bool show)
+    {
+        m_prevShowImGui = m_showImGui;
+        if (show)
+        {
+            AZ::Render::ImGuiSystemRequestBus::Broadcast(&AZ::Render::ImGuiSystemRequestBus::Events::ShowAllImGuiPasses);
+        }
+        else
+        {
+            AZ::Render::ImGuiSystemRequestBus::Broadcast(&AZ::Render::ImGuiSystemRequestBus::Events::HideAllImGuiPasses);
+        }
+        m_showImGui = show;
+    }
+
+    void ScriptManager::Script_SetShowImGui(bool show)
+    {
+        auto operation = [show]()
+        {
+            s_instance->SetShowImGui(show);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    bool ScriptManager::PrepareForScreenCapture(const AZStd::string& path)
+    {
+
+        if (!Utils::IsFileUnderFolder(Utils::ResolvePath(path), ScreenshotPaths::GetScreenshotsFolder(true)))
+        {
+            // The main reason we require screenshots to be in a specific folder is to ensure we don't delete or replace some other important file.
+            ReportScriptError(AZStd::string::format(
+                "Screenshots must be captured under the '%s' folder. Attempted to save screenshot to '%s'.",
+                ScreenshotPaths::GetScreenshotsFolder(false).c_str(), path.c_str()));
+
+            return false;
+        }
+
+        // Delete the file if it already exists because if the screen capture fails, we don't want to do a screenshot comparison test using an old screenshot.
+        if (AZ::IO::LocalFileIO::GetInstance()->Exists(path.c_str()) && !AZ::IO::LocalFileIO::GetInstance()->Remove(path.c_str()))
+        {
+            ReportScriptError(AZStd::string::format("Failed to delete existing screenshot file '%s'.", path.c_str()));
+            return false;
+        }
+
+        s_instance->m_scriptReporter.AddScreenshotTest(path);
+
+        s_instance->m_isCapturePending = true;
+        s_instance->AZ::Render::FrameCaptureNotificationBus::Handler::BusConnect();
+        s_instance->PauseScript();
+        
+        return true;
+    }
+
+    void ScriptManager::Script_SelectImageComparisonToleranceLevel(const AZStd::string& presetName)
+    {
+        auto operation = [presetName]()
+        {
+            s_instance->m_imageComparisonOptions.SelectToleranceLevel(presetName);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_CaptureScreenshot(const AZStd::string& filePath)
+    {
+        Script_SetShowImGui(false);
+
+        auto operation = [filePath]()
+        {
+            // Note this will pause the script until the capture is complete
+            if (PrepareForScreenCapture(filePath))
+            {
+                AZ::Render::FrameCaptureRequestBus::Broadcast(&AZ::Render::FrameCaptureRequestBus::Events::CaptureScreenshot, filePath);
+            }
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+        s_instance->m_scriptOperations.push([]()
+            {
+                s_instance->m_scriptReporter.CheckLatestScreenshot(s_instance->m_imageComparisonOptions.GetCurrentToleranceLevel());
+            });
+
+        // restore imgui show/hide
+        s_instance->m_scriptOperations.push([]()
+            {
+                s_instance->SetShowImGui(s_instance->m_prevShowImGui);
+            });
+
+    }
+
+    void ScriptManager::Script_CaptureScreenshotWithImGui(const AZStd::string& filePath)
+    {
+        Script_SetShowImGui(true);
+
+        auto operation = [filePath]()
+        {
+            // Note this will pause the script until the capture is complete
+            if (PrepareForScreenCapture(filePath))
+            {
+                AZ::Render::FrameCaptureRequestBus::Broadcast(&AZ::Render::FrameCaptureRequestBus::Events::CaptureScreenshot, filePath);
+            }
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+        s_instance->m_scriptOperations.push([]()
+            {
+                s_instance->m_scriptReporter.CheckLatestScreenshot(s_instance->m_imageComparisonOptions.GetCurrentToleranceLevel());
+            });
+
+        // restore imgui show/hide
+        s_instance->m_scriptOperations.push([]()
+            {
+                s_instance->SetShowImGui(s_instance->m_prevShowImGui);
+            });
+    }
+
+    void ScriptManager::Script_CaptureScreenshotWithPreview(const AZStd::string& filePath)
+    {
+        auto operation = [filePath]()
+        {
+            // Note this will pause the script until the capture is complete
+            if (PrepareForScreenCapture(filePath))
+            {
+                AZ::Render::FrameCaptureRequestBus::Broadcast(&AZ::Render::FrameCaptureRequestBus::Events::CaptureScreenshotWithPreview, filePath);
+            }
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+        s_instance->m_scriptOperations.push([]()
+            {
+                s_instance->m_scriptReporter.CheckLatestScreenshot(s_instance->m_imageComparisonOptions.GetCurrentToleranceLevel());
+            });
+    }
+
+    void ScriptManager::Script_CapturePassAttachment(AZ::ScriptDataContext& dc)
+    {
+        if (dc.GetNumArguments() != 3)
+        {
+            ReportScriptError("CapturePassAttachment needs three arguments");
+            return;
+        }
+
+        if (!dc.IsTable(0))
+        {
+            ReportScriptError("CapturePassAttachment's first argument must be a table of strings");
+            return;
+        }
+
+        if (!dc.IsString(1) || !dc.IsString(2))
+        {
+            ReportScriptError("CapturePassAttachment's second and third argument must be strings");
+            return;
+        }
+
+        const char* stringValue = nullptr;
+
+        AZStd::vector<AZStd::string> passHierarchy;
+        AZStd::string slot;
+        AZStd::string outputFilePath;
+
+        // read slot name and output file path
+        dc.ReadArg(1, stringValue);
+        slot = AZStd::string(stringValue);
+        dc.ReadArg(2, stringValue);
+        outputFilePath = AZStd::string(stringValue);
+
+        // read pass hierarchy
+        AZ::ScriptDataContext stringtable;
+        dc.InspectTable(0, stringtable);
+
+        const char* fieldName;
+        int fieldIndex;
+        int elementIndex;
+
+        while (stringtable.InspectNextElement(elementIndex, fieldName, fieldIndex))
+        {
+            if (fieldIndex != -1)
+            {
+                if (!stringtable.IsString(elementIndex))
+                {
+                    ReportScriptError("CapturePassAttachment's first argument must contain only strings");
+                    return;
+                }
+
+                const char* stringTableValue = nullptr;
+                if (stringtable.ReadValue(elementIndex, stringTableValue))
+                {
+                    passHierarchy.push_back(stringTableValue);
+                }
+            }
+        }
+
+        auto operation = [passHierarchy, slot, outputFilePath]()
+        {
+            // Note this will pause the script until the capture is complete
+            if (PrepareForScreenCapture(outputFilePath))
+            {
+                AZ::Render::FrameCaptureRequestBus::Broadcast(&AZ::Render::FrameCaptureRequestBus::Events::CapturePassAttachment, passHierarchy, slot, outputFilePath);
+            }
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+        s_instance->m_scriptOperations.push([]()
+            {
+                s_instance->m_scriptReporter.CheckLatestScreenshot(s_instance->m_imageComparisonOptions.GetCurrentToleranceLevel());
+            });
+    }
+
+    void ScriptManager::OnCaptureFinished(AZ::Render::FrameCaptureResult result, const AZStd::string &info)
+    {
+        m_isCapturePending = false;
+        AZ::Render::FrameCaptureNotificationBus::Handler::BusDisconnect();
+        ResumeScript();
+
+        // This is checking for the exact scenario that results from an HDR setup. The goal is to add a very specific and prominent message that will
+        // alert users to a common issue and what action to take. Any other Format issues will be reported by FrameCaptureSystemComponent with a
+        // "Can't save image with format %s to a ppm file" message.
+        if (result == AZ::Render::FrameCaptureResult::UnsupportedFormat && info.find(AZ::RHI::ToString(AZ::RHI::Format::R10G10B10A2_UNORM)) != AZStd::string::npos)
+        {
+            m_messageBox.OpenPopupMessage("HDR Not Supported", "Screen capture testing is not supported in HDR. Please change the system configuration to disable the HDR display feature.");
+            AbortScripts("Script(s) aborted due to HDR configuration.");
+        }
+    }
+
+    void ScriptManager::OnCaptureQueryTimestampFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
+    {
+        m_isCapturePending = false;
+        AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
+        ResumeScript();
+    }
+
+    void ScriptManager::OnCaptureQueryPipelineStatisticsFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
+    {
+        m_isCapturePending = false;
+        AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
+        ResumeScript();
+    }
+
+    void ScriptManager::OnCaptureCpuProfilingStatisticsFinished([[maybe_unused]] bool result, [[maybe_unused]] const AZStd::string& info)
+    {
+        m_isCapturePending = false;
+        AZ::Render::ProfilingCaptureNotificationBus::Handler::BusDisconnect();
+        ResumeScript();
+    }
+
+    void ScriptManager::Script_CapturePassTimestamp(AZ::ScriptDataContext& dc)
+    {
+        AZStd::string outputFilePath;
+        const bool readScriptDataContext = ValidateProfilingCaptureScripContexts(dc, outputFilePath);
+        if (!readScriptDataContext)
+        {
+            return;
+        }
+
+        auto operation = [outputFilePath]()
+        {
+            s_instance->m_isCapturePending = true;
+            s_instance->AZ::Render::ProfilingCaptureNotificationBus::Handler::BusConnect();
+            s_instance->PauseScript();
+
+            AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CapturePassTimestamp, outputFilePath);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_CapturePassPipelineStatistics(AZ::ScriptDataContext& dc)
+    {
+        AZStd::string outputFilePath;
+        const bool readScriptDataContext = ValidateProfilingCaptureScripContexts(dc, outputFilePath);
+        if (!readScriptDataContext)
+        {
+            return;
+        }
+
+        auto operation = [outputFilePath]()
+        {
+            s_instance->m_isCapturePending = true;
+            s_instance->AZ::Render::ProfilingCaptureNotificationBus::Handler::BusConnect();
+            s_instance->PauseScript();
+
+            AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CapturePassPipelineStatistics, outputFilePath);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_CaptureCpuProfilingStatistics(AZ::ScriptDataContext& dc)
+    {
+        AZStd::string outputFilePath;
+        const bool readScriptDataContext = ValidateProfilingCaptureScripContexts(dc, outputFilePath);
+        if (!readScriptDataContext)
+        {
+            return;
+        }
+
+        auto operation = [outputFilePath]()
+        {
+            s_instance->m_isCapturePending = true;
+            s_instance->AZ::Render::ProfilingCaptureNotificationBus::Handler::BusConnect();
+            s_instance->PauseScript();
+
+            AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics, outputFilePath);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    bool ScriptManager::ValidateProfilingCaptureScripContexts(AZ::ScriptDataContext& dc, AZStd::string& outputFilePath)
+    {
+        if (dc.GetNumArguments() != 1)
+        {
+            ReportScriptError("ProfilingCaptureScriptDataContext needs one argument");
+            return false;
+        }
+
+        if (!dc.IsString(0))
+        {
+            ReportScriptError("ProfilingCaptureScriptDataContext's first (and only) argument must be of type string");
+            return false;
+        }
+
+        // Read slot name and output file path
+        const char* stringValue = nullptr;
+        dc.ReadArg(0, stringValue);
+        if (stringValue == nullptr)
+        {
+            ReportScriptError("ProfilingCaptureScriptDataContext failed to read the string value");
+            return false;
+        }
+
+        outputFilePath = AZStd::string(stringValue);
+        return true;
+    }
+
+    float ScriptManager::Script_DegToRad(float degrees)
+    {
+        return AZ::DegToRad(degrees);
+    }
+
+    AZStd::string ScriptManager::Script_GetRenderApiName()
+    {
+        AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get();
+        return rpiSystem->GetRenderApiName().GetCStr();
+        
+    }
+
+    void ScriptManager::CheckArcBallControllerHandler()
+    {
+        if (0 == AZ::Debug::ArcBallControllerRequestBus::GetNumOfEventHandlers(s_instance->m_cameraEntity->GetId()))
+        {
+            ReportScriptError("There is no handler for ArcBallControllerRequestBus for the camera entity.");
+        }
+    }
+
+    void ScriptManager::CheckNoClipControllerHandler()
+    {
+        if (0 == AZ::Debug::NoClipControllerRequestBus::GetNumOfEventHandlers(s_instance->m_cameraEntity->GetId()))
+        {
+            ReportScriptError("There is no handler for NoClipControllerRequestBus for the camera entity.");
+        }
+    }
+
+    void ScriptManager::Script_ArcBallCameraController_SetCenter(AZ::Vector3 center)
+    {
+        auto operation = [center]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckArcBallControllerHandler();
+            AZ::Debug::ArcBallControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetCenter, center);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_ArcBallCameraController_SetPan(AZ::Vector3 pan)
+    {
+        auto operation = [pan]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckArcBallControllerHandler();
+            AZ::Debug::ArcBallControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetPan, pan);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_ArcBallCameraController_SetDistance(float distance)
+    {
+        auto operation = [distance]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckArcBallControllerHandler();
+            AZ::Debug::ArcBallControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetDistance, distance);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_ArcBallCameraController_SetHeading(float heading)
+    {
+        auto operation = [heading]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckArcBallControllerHandler();
+            AZ::Debug::ArcBallControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetHeading, heading);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_ArcBallCameraController_SetPitch(float pitch)
+    {
+        auto operation = [pitch]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckArcBallControllerHandler();
+            AZ::Debug::ArcBallControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetPitch, pitch);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_NoClipCameraController_SetPosition(AZ::Vector3 position)
+    {
+        auto operation = [position]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckNoClipControllerHandler();
+            AZ::Debug::NoClipControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetPosition, position);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_NoClipCameraController_SetHeading(float heading)
+    {
+        auto operation = [heading]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckNoClipControllerHandler();
+            AZ::Debug::NoClipControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetHeading, heading);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_NoClipCameraController_SetPitch(float pitch)
+    {
+        auto operation = [pitch]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckNoClipControllerHandler();
+            AZ::Debug::NoClipControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetPitch, pitch);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_NoClipCameraController_SetFov(float fov)
+    {
+        auto operation = [fov]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            CheckNoClipControllerHandler();
+            AZ::Debug::NoClipControllerRequestBus::Event(s_instance->m_cameraEntity->GetId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetFov, fov);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_AssetTracking_Start()
+    {
+        auto operation = []()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            s_instance->m_assetStatusTracker.StartTracking();
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+
+    void ScriptManager::Script_AssetTracking_ExpectAsset(const AZStd::string& sourceAssetPath, uint32_t expectedCount)
+    {
+        auto operation = [sourceAssetPath, expectedCount]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            s_instance->m_assetStatusTracker.ExpectAsset(sourceAssetPath, expectedCount);
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_AssetTracking_IdleUntilExpectedAssetsFinish(float timeout)
+    {
+        auto operation = [timeout]()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+            AZ_Assert(!s_instance->m_waitForAssetTracker, "It shouldn't be possible to run the next command until m_waitForAssetTracker is false");
+
+            s_instance->m_waitForAssetTracker = true;
+            s_instance->m_assetTrackingTimeout = timeout;
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+
+    void ScriptManager::Script_AssetTracking_Stop()
+    {
+        auto operation = []()
+        {
+            AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+            s_instance->m_assetStatusTracker.StopTracking();
+        };
+
+        s_instance->m_scriptOperations.push(AZStd::move(operation));
+    }
+} // namespace AtomSampleViewer

+ 325 - 0
Gem/Code/Source/Automation/ScriptManager.h

@@ -0,0 +1,325 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <Atom/Component/DebugCamera/CameraControllerBus.h>
+#include <Atom/Feature/Utils/FrameCaptureBus.h>
+#include <Atom/Feature/Utils/ProfilingCaptureBus.h>
+#include <Automation/ScriptRepeaterBus.h>
+#include <Automation/ScriptRunnerBus.h>
+#include <Automation/AssetStatusTracker.h>
+#include <Automation/ScriptReporter.h>
+#include <Automation/ImageComparisonConfig.h>
+#include <Utils/ImGuiAssetBrowser.h>
+
+namespace AZ 
+{
+    class ScriptContext;
+    class ScriptDataContext;
+    class ScriptAsset;
+}
+
+namespace AtomSampleViewer
+{
+    //! Manages running lua scripts for test automation.
+    //! This initializes a lua context, binds C++ callback functions, does per-frame processing to execute
+    //! scripts, manages the ScriptableImGui utility, and provides an ImGui dialog for opening and running scripts.
+    //!
+    //! This uses an asynchronous execution model, which is necessary in order to allow scripts
+    //! to simply call functions like IdleFrames() or IdleSeconds() to insert delays, making scripts much
+    //! easier to write. When a script runs, every callback function adds an entry to an operations queue,
+    //! and the Tick() function works its way through this queue every frame.
+    //! Note that this means the C++ functions we expose to lua cannot return dynamic data; the only data we can
+    //! return are constants like the number of samples available, or stateless utility functions like DegToRad().
+    //!
+    //! (Eventually we could improve this by putting the lua execution in the top-level application loop code, in
+    //!  AtomSampleViewerApplication::Tick() and make the "Idle" functions pump AtomSampleViewerApplication::Tick() manually.
+    //!  But that is only used in AtomSampleViewer.exe, not AtomSampleViewerLauncher.exe which is necessary for cross platform
+    //!  testing. Someday we'll probably make AtomSampleViewer.exe work across multiple platforms and then we could change
+    //!  our scripting execution model).
+    class ScriptManager final
+        : public ScriptRepeaterRequestBus::Handler
+        , public ScriptRunnerRequestBus::Handler
+        , public AZ::Debug::CameraControllerNotificationBus::Handler
+        , public AZ::Render::FrameCaptureNotificationBus::Handler
+        , public AZ::Render::ProfilingCaptureNotificationBus::Handler
+    {
+    public:
+        ScriptManager();
+
+        void Activate();
+        void Deactivate();
+        
+        void SetCameraEntity(AZ::Entity* cameraEntity);
+
+        void TickScript(float deltaTime);
+        void TickImGui();
+
+        void OpenScriptRunnerDialog();
+
+        void RunMainTestSuite(const AZStd::string& suiteFilePath, bool exitOnTestEnd);
+        
+    private:
+
+        void ShowScriptRunnerDialog();
+
+        // Registers functions in a BehaviorContext so they can be exposed to Lua scripts.
+        static void ReflectScriptContext(AZ::BehaviorContext* context);
+
+        ///////////////////////////////////////////////////////////////////////
+        // Script callback functions...
+
+        // Utilities...
+        static void Script_RunScript(const AZStd::string& scriptFilePath);
+        static void Script_Error(const AZStd::string& message);
+        static void Script_Warning(const AZStd::string& message);
+        static void Script_Print(const AZStd::string& message);
+        static void Script_IdleFrames(int numFrames);
+        static void Script_IdleSeconds(float numSeconds);
+        static void Script_LockFrameTime(float seconds);
+        static void Script_UnlockFrameTime();
+        static void Script_ResizeViewport(int width, int height);
+        static void Script_ExecuteConsoleCommand(const AZStd::string& command);
+
+        // Show/Hide ImGui
+        static void Script_SetShowImGui(bool show);
+
+        // Utilities returning data (these special functions do return data because they don't read dynamic state)...
+        static AZStd::string Script_ResolvePath(const AZStd::string& path);
+        static AZStd::string Script_NormalizePath(const AZStd::string& path);
+        static float Script_DegToRad(float degrees);
+        static AZStd::string Script_GetRenderApiName();
+
+        // Samples...
+        static void Script_OpenSample(const AZStd::string& sampleName);
+        static void Script_SetImguiValue(AZ::ScriptDataContext& dc);
+
+        // Debug tools...
+        // Show or hide a debug tool with the given name
+        static void Script_ShowTool(const AZStd::string& toolName, bool enable);
+
+        // Screenshots...
+
+        // Call this function before capturing screenshots to indicate which comparison tolerance level should be used.
+        // The list of available tolerance levels can be found in "AtomSampleViewer/Config/ImageComparisonToleranceLevels.azasset".
+        static void Script_SelectImageComparisonToleranceLevel(const AZStd::string& toleranceLevelName);
+
+        // All of the following functions capture a frame and save it to the given file path.
+        // The path must be under the "Scripts/Screenshots" folder and have extension ".ppm".
+        // If screenshot comparison testing is enabled, this will also check the captured image against a
+        // baseline image file. The function will assume the corresponding expected image will be in a similar path,
+        // but with "Screenshots" replaced with "ExpectedScreenshots". For example, the expected file for
+        // "Scripts/Screenshots/StandardPbr/test.ppm" should be at "Scripts/ExpectedScreenshots/StandardPbr/test.ppm".
+
+        static void Script_CaptureScreenshot(const AZStd::string& filePath);
+        static void Script_CaptureScreenshotWithImGui(const AZStd::string& filePath);
+
+        // Capture a pass attachment and save it to a file (*.ppm or *.dds for image, *.buffer for buffer)
+        // The order of input parameters in ScriptDataContext would be
+        // 0: table of strings for pass hierarchy
+        // 1: string for the slot name
+        // 2: string for output file path
+        // Check the description of FrameCaptureRequests::CapturePassAttachment function for detail
+        static void Script_CapturePassAttachment(AZ::ScriptDataContext& dc);
+
+        // Capture a screentshot with pass image attachment preview when the preview enabled.
+        static void Script_CaptureScreenshotWithPreview(const AZStd::string& filePath);
+
+        // Profiling statistics data...
+        static void Script_CapturePassTimestamp(AZ::ScriptDataContext& dc);
+        static void Script_CapturePassPipelineStatistics(AZ::ScriptDataContext& dc);
+        static void Script_CaptureCpuProfilingStatistics(AZ::ScriptDataContext& dc);
+
+        // Camera...
+        static void Script_ArcBallCameraController_SetCenter(AZ::Vector3 center);
+        static void Script_ArcBallCameraController_SetPan(AZ::Vector3 pan);
+        static void Script_ArcBallCameraController_SetDistance(float distance);
+        static void Script_ArcBallCameraController_SetHeading(float heading);
+        static void Script_ArcBallCameraController_SetPitch(float pitch);
+        static void Script_NoClipCameraController_SetPosition(AZ::Vector3 center);
+        static void Script_NoClipCameraController_SetHeading(float heading);
+        static void Script_NoClipCameraController_SetPitch(float pitch);
+        static void Script_NoClipCameraController_SetFov(float fov);
+
+        // Asset System...
+
+        // Starts tracking asset status updates from the Asset Processor. Clears any asset status information already collected.
+        static void Script_AssetTracking_Start();
+
+        // Sets the AssetStatusTracker to expect a particular asset with specific expected results.
+        // Note this can be called multiple times with the same assetPath, in which case the expected counts will be added.
+        // @param sourceAssetPath the source asset path, relative to the watch folder. Will be normalized and matched case-insensitive.
+        // @param expectedCount number of completed jobs expected for this asset.
+        static void Script_AssetTracking_ExpectAsset(const AZStd::string& sourceAssetPath, uint32_t expectedCount);
+
+        // The system will idle until all expected assets have been reported as finished, either succeeded or failed.
+        // Returns immediately if all the assets ware already finished any time since AssetTracking_Start() was called.
+        // @param timeout Float timeout for the idle operation
+        static void Script_AssetTracking_IdleUntilExpectedAssetsFinish(float timeout);
+
+        // Stops tracking asset status updates from the Asset Processor. Clears any asset status information already collected.
+        static void Script_AssetTracking_Stop();
+
+        ///////////////////////////////////////////////////////////////////////
+
+        static void CheckArcBallControllerHandler();
+        static void CheckNoClipControllerHandler();
+
+        // Similar to Script_Error, but reports the message immediately
+        static void ReportScriptError(const AZStd::string& message);
+
+        // Similar to Script_Warning, but reports the message immediately
+        static void ReportScriptWarning(const AZStd::string& message);
+
+        // ScriptRunnerRequestBus overrides...
+        void PauseScript() override;
+        void PauseScriptWithTimeout(float timeout) override;
+        void ResumeScript() override;
+
+        // Execute a lua script. Each function call in the script will call one of the above Script_ functions,
+        // which will push operations onto the m_scriptOperations queue for deferred execution in TickScript().
+        void ExecuteScript(const AZStd::string& scriptFilePath);
+
+        // Prepare test environment and logging and then execute the script
+        void PrepareAndExecuteScript(const AZStd::string& scriptFilePath);
+
+        // ScriptRepeaterRequestBus overrides...
+        void ReportScriptableAction(AZStd::string_view scriptCommand) override;
+
+        // CameraControllerNotificationBus overrides...
+        void OnCameraMoveEnded(AZ::TypeId controllerTypeId, uint32_t channels) override;
+
+        // FrameCaptureNotificationBus overrides...
+        void OnCaptureFinished(AZ::Render::FrameCaptureResult result, const AZStd::string& info) override;
+
+        // ProfilingCaptureNotificationBus overrides...
+        void OnCaptureQueryTimestampFinished(bool result, const AZStd::string& info) override;
+        void OnCaptureQueryPipelineStatisticsFinished(bool result, const AZStd::string& info) override;
+        void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) override;
+
+        void AbortScripts(const AZStd::string& reason);
+
+        // Validates the ScriptDataContext for ProfilingCapture script requests
+        static bool ValidateProfilingCaptureScripContexts(AZ::ScriptDataContext& dc, AZStd::string& outputFilePath);
+
+        static bool PrepareForScreenCapture(const AZStd::string& path);
+
+        // show/hide imgui
+        void SetShowImGui(bool show);
+
+        struct TestSuiteExecutionConfig
+        {
+            bool m_automatedRunEnabled = false;
+            bool m_isStarted = false;
+            bool m_closeOnTestScriptFinish = false;
+            AZStd::string m_testSuitePath;
+        };
+
+        TestSuiteExecutionConfig m_testSuiteRunConfig;
+
+        static constexpr float DefaultPauseTimeout = 5.0f;
+
+        int m_scriptIdleFrames = 0;
+        float m_scriptIdleSeconds = 0.0f;
+        bool m_scriptPaused = false;
+        float m_scriptPauseTimeout = 0.0f;
+
+        bool m_waitForAssetTracker = false;
+        float m_assetTrackingTimeout = 0.0f;
+        AssetStatusTracker m_assetStatusTracker;
+
+        AZStd::unique_ptr<AZ::ScriptContext> m_scriptContext; //< Provides the lua scripting system
+        AZStd::unique_ptr<AZ::BehaviorContext> m_sriptBehaviorContext; //< Used to bind script callback functions to lua
+
+        bool m_shouldRestoreViewportSize = false;
+        int m_savedViewportWidth = 0;
+        int m_savedViewportHeight = 0;
+
+        AZ::Entity* m_cameraEntity = nullptr;
+
+        ImGuiMessageBox m_messageBox;
+
+        using ScriptOp = AZStd::function<void()>;
+        AZStd::queue<ScriptOp> m_scriptOperations;
+        bool m_doFinalScriptCleanup = false;
+
+        ImGuiAssetBrowser m_scriptBrowser;
+
+        AZStd::unordered_set<AZ::Data::AssetId> m_executingScripts; //< Tracks which lua scripts are currently being executed. Used to prevent infinite recursion.
+        bool m_shouldPopScript = false; //< Tracks when an executing script just finished so we know when to call ScriptReporter::PopScript().
+        ScriptReporter m_scriptReporter;
+
+        // Manages the available ImageComparisonToleranceLevels and override options
+        class ImageComparisonOptions : public AZ::Data::AssetBus::Handler
+        {
+        public:
+            void Activate();
+            void Deactivate();
+
+            //! Return the tolerance level with the given name.
+            //! The returned level may be adjusted according to the user's "Level Adjustment" setting in ImGui.
+            //! @param name name of the tolerance level to find.
+            //! @param allowLevelAdjustment may be set to false to avoid applying the user's "Level Adjustment" setting.
+            ImageComparisonToleranceLevel* FindToleranceLevel(const AZStd::string& name, bool allowLevelAdjustment = true);
+
+            //! Returns the list of all available tolerance levels, sorted most- to least-strict.
+            const AZStd::vector<ImageComparisonToleranceLevel>& GetAvailableToleranceLevels() const;
+
+            //! Sets the active tolerance level.
+            //! The selected level may be adjusted according to the user's "Level Adjustment" setting in ImGui.
+            //! @param name name of the tolerance level to select.
+            //! @param allowLevelAdjustment may be set to false to avoid applying the user's "Level Adjustment" setting.
+            void SelectToleranceLevel(const AZStd::string& name, bool allowLevelAdjustment = true);
+
+            //! Sets the active tolerance level.
+            //! @param level must be one of the tolerance levels already available.
+            void SelectToleranceLevel(ImageComparisonToleranceLevel* level);
+
+            //! Returns the active tolerance level.
+            ImageComparisonToleranceLevel* GetCurrentToleranceLevel();
+
+            //! Returns whether the user has configured the script to control tolerance
+            //! level selection, otherwise they have selected a specific override level.
+            bool IsScriptControlled() const;
+
+            //! Returns true if the user has applied a level up/down adjustment in ImGui.
+            bool IsLevelAdjusted() const;
+
+            void DrawImGuiSettings();
+            void ResetImGuiSettings();
+
+        private:
+
+            // AssetBus overrides...
+            void OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
+
+            AZ::Data::Asset<AZ::RPI::AnyAsset> m_configAsset;
+            ImageComparisonConfig m_config;
+            ImageComparisonToleranceLevel* m_currentToleranceLevel = nullptr;
+            static constexpr int OverrideSetting_ScriptControlled = 0;
+            AZStd::vector<const char*> m_overrideSettings;
+            int m_selectedOverrideSetting = 0;
+            int m_toleranceAdjustment = 0;
+
+        } m_imageComparisonOptions;
+
+        bool m_showScriptRunnerDialog = false;
+        bool m_isCapturePending = false;
+        bool m_frameTimeIsLocked = false;
+
+        bool m_prevShowImGui = true;
+        bool m_showImGui = true;
+
+        static ScriptManager* s_instance;
+    };
+} // namespace AtomSampleViewer

+ 32 - 0
Gem/Code/Source/Automation/ScriptRepeaterBus.h

@@ -0,0 +1,32 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/std/string/string_view.h>
+#include <AzCore/EBus/EBus.h>
+
+namespace AtomSampleViewer
+{
+    class ScriptRepeaterRequests
+        : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
+
+        //! Reports a snippet of script that can be run to repeat some action that was just done by the user.
+        virtual void ReportScriptableAction(AZStd::string_view scriptCommand) = 0;
+    };
+
+    using ScriptRepeaterRequestBus = AZ::EBus<ScriptRepeaterRequests>;
+
+} // namespace AtomSampleViewer

+ 1096 - 0
Gem/Code/Source/Automation/ScriptReporter.cpp

@@ -0,0 +1,1096 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Automation/ScriptReporter.h>
+#include <Utils/Utils.h>
+#include <Atom/Utils/PpmFile.h>
+#include <imgui/imgui.h>
+#include <Atom/RHI/Factory.h>
+#include <AzFramework/API/ApplicationAPI.h>
+#include <AzFramework/StringFunc/StringFunc.h>
+#include <AzFramework/IO/LocalFileIO.h>
+#include <AzCore/IO/SystemFile.h>
+#include <AzCore/Utils/Utils.h>
+
+namespace AtomSampleViewer
+{
+    // Must match ScriptReporter::DisplayOption Enum
+    static const char* DiplayOptions[] =
+    {
+        "All Results", "Warnings & Errors", "Errors Only",
+    };
+
+    namespace ScreenshotPaths
+    {
+        AZStd::string GetScreenshotsFolder(bool resolvePath)
+        {
+            AZStd::string path = "@user@/scripts/screenshots/";
+
+            if (resolvePath)
+            {
+                path = Utils::ResolvePath(path);
+            }
+
+            return path;
+        }
+
+        AZStd::string GetLocalBaselineFolder(bool resolvePath)
+        {
+            AZStd::string path = AZStd::string::format("@user@/scripts/screenshotslocalbaseline/%s", AZ::RHI::Factory::Get().GetName().GetCStr());
+
+            if (resolvePath)
+            {
+                path = Utils::ResolvePath(path);
+            }
+
+            return path;
+        }
+
+        AZStd::string GetOfficialBaselineFolder(bool resolvePath)
+        {
+            AZStd::string path = "scripts/expectedscreenshots/";
+
+            if (resolvePath)
+            {
+                path = Utils::ResolvePath(path);
+            }
+
+            return path;
+        }
+
+        AZStd::string GetLocalBaseline(const AZStd::string& forScreenshotFile)
+        {
+            AZStd::string localBaselineFolder = GetLocalBaselineFolder(false);
+            AzFramework::StringFunc::Replace(localBaselineFolder, "@user@/", "");
+            AZStd::string newPath = forScreenshotFile;
+            if (!AzFramework::StringFunc::Replace(newPath, "scripts/screenshots", localBaselineFolder.c_str()))
+            {
+                newPath = "";
+            }
+            return newPath;
+        }
+
+        AZStd::string GetOfficialBaseline(const AZStd::string& forScreenshotFile)
+        {
+            AZStd::string newPath = forScreenshotFile;
+            if (!AzFramework::StringFunc::Replace(newPath, "user/scripts/screenshots", "atomsampleviewer/scripts/expectedscreenshots"))
+            {
+                newPath = "";
+            }
+            return newPath;
+        }
+    }
+
+    AZStd::string ScriptReporter::ImageComparisonResult::GetSummaryString() const
+    {
+        AZStd::string resultString;
+
+        if (m_resultCode == ResultCode::ThresholdExceeded || m_resultCode == ResultCode::Pass)
+        {
+            resultString = AZStd::string::format("Diff Score: %f", m_finalDiffScore);
+        }
+        else if (m_resultCode == ResultCode::WrongSize)
+        {
+            resultString = "Wrong size";
+        }
+        else if (m_resultCode == ResultCode::FileNotFound)
+        {
+            resultString = "File not found";
+        }
+        else if (m_resultCode == ResultCode::FileNotLoaded)
+        {
+            resultString = "File load failed";
+        }
+        else if (m_resultCode == ResultCode::WrongFormat)
+        {
+            resultString = "Format is not supported";
+        }
+        else if (m_resultCode == ResultCode::NullImageComparisonToleranceLevel)
+        {
+            resultString = "ImageComparisonToleranceLevel not provided";
+        }
+        else if (m_resultCode == ResultCode::None)
+        {
+            // "None" could be the case if the results dialog is open while the script is running
+            resultString = "No results";
+        }
+        else 
+        {
+            resultString = "Unhandled Image Comparison ResultCode";
+            AZ_Assert(false, "Unhandled Image Comparison ResultCode");
+        }
+
+        return resultString;
+    }
+
+    void ScriptReporter::SetAvailableToleranceLevels(const AZStd::vector<ImageComparisonToleranceLevel>& toleranceLevels)
+    {
+        m_availableToleranceLevels = toleranceLevels;
+    }
+
+    void ScriptReporter::Reset()
+    {
+        m_scriptReports.clear();
+        m_currentScriptIndexStack.clear();
+        m_invalidationMessage.clear();
+    }
+
+    void ScriptReporter::SetInvalidationMessage(const AZStd::string& message)
+    {
+        m_invalidationMessage = message;
+
+        // Reporting this message here instead of when running the script so it won't show up as an error in the ImGui report.
+        AZ_Error("Automation", m_invalidationMessage.empty(), "Subsequent test results will be invalid because '%s'", m_invalidationMessage.c_str());
+    }
+
+    void ScriptReporter::PushScript(const AZStd::string& scriptAssetPath)
+    {
+        if (GetCurrentScriptReport())
+        {
+            // Only the current script should listen for Trace Errors
+            GetCurrentScriptReport()->BusDisconnect();
+        }
+
+        m_currentScriptIndexStack.push_back(m_scriptReports.size());
+        m_scriptReports.push_back();
+        m_scriptReports.back().m_scriptAssetPath = scriptAssetPath;
+        m_scriptReports.back().BusConnect();
+    }
+
+    void ScriptReporter::PopScript()
+    {
+        AZ_Assert(GetCurrentScriptReport(), "There is no active script");
+
+        if (GetCurrentScriptReport())
+        {
+            GetCurrentScriptReport()->BusDisconnect();
+            m_currentScriptIndexStack.pop_back();
+        }
+
+        if (GetCurrentScriptReport())
+        {
+            // Make sure the newly restored current script is listening for Trace Errors
+            GetCurrentScriptReport()->BusConnect();
+        }
+    }
+
+    bool ScriptReporter::HasActiveScript() const
+    {
+        return !m_currentScriptIndexStack.empty();
+    }
+
+    bool ScriptReporter::AddScreenshotTest(const AZStd::string& path)
+    {
+        AZ_Assert(GetCurrentScriptReport(), "There is no active script");
+
+        ScreenshotTestInfo screenshotTestInfo;
+        screenshotTestInfo.m_screenshotFilePath = path;
+        GetCurrentScriptReport()->m_screenshotTests.push_back(AZStd::move(screenshotTestInfo));
+
+        return true;
+    }
+
+    void ScriptReporter::TickImGui()
+    {
+        if (m_showReportDialog)
+        {
+            ShowReportDialog();
+        }
+    }
+
+    bool ScriptReporter::HasErrorsAssertsInReport() const
+    {
+        for (const ScriptReport& scriptReport : m_scriptReports)
+        {
+            if (scriptReport.m_assertCount > 0 || scriptReport.m_generalErrorCount > 0 || scriptReport.m_screenshotErrorCount > 0)
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    void ScriptReporter::ShowDiffButton(const char* buttonLabel, const AZStd::string& imagePathA, const AZStd::string& imagePathB)
+    {
+        if (ImGui::Button(buttonLabel))
+        {
+            if (!Utils::RunDiffTool(imagePathA, imagePathB))
+            {
+                m_messageBox.OpenPopupMessage("Can't Diff", "Image diff is not supported on this platform, or the required diff tool is not installed.");
+            }
+        }
+    }
+
+    const ImageComparisonToleranceLevel* ScriptReporter::FindBestToleranceLevel(float diffScore, bool filterImperceptibleDiffs) const
+    {
+        float thresholdChecked = 0.0f;
+        bool ignoringMinorDiffs = false;
+        for (const ImageComparisonToleranceLevel& level : m_availableToleranceLevels)
+        {
+            AZ_Assert(level.m_threshold > thresholdChecked || thresholdChecked == 0.0f, "Threshold values are not sequential");
+            AZ_Assert(level.m_filterImperceptibleDiffs >= ignoringMinorDiffs, "filterImperceptibleDiffs values are not sequential");
+            thresholdChecked = level.m_threshold;
+            ignoringMinorDiffs = level.m_filterImperceptibleDiffs;
+
+            if (filterImperceptibleDiffs <= level.m_filterImperceptibleDiffs && diffScore <= level.m_threshold)
+            {
+                return &level;
+            }
+        }
+
+        return nullptr;
+    }
+
+    void ScriptReporter::ShowReportDialog()
+    {
+        if (ImGui::Begin("Script Results", &m_showReportDialog) && !m_scriptReports.empty())
+        {
+            const ImVec4& bgColor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
+            const bool isDarkStyle = bgColor.x < 0.2 && bgColor.y < 0.2 && bgColor.z < 0.2;
+            const ImVec4 HighlightPassed = isDarkStyle ? ImVec4{0.5, 1, 0.5, 1} : ImVec4{0, 0.75, 0, 1};
+            const ImVec4 HighlightFailed = isDarkStyle ? ImVec4{1, 0.5, 0.5, 1} : ImVec4{0.75, 0, 0, 1};
+            const ImVec4 HighlightWarning = isDarkStyle ? ImVec4{1, 1, 0.5, 1} : ImVec4{0.5, 0.5, 0, 1};
+
+            // Local utilities for setting text color
+            bool colorHasBeenSet = false;
+            auto highlightTextIf = [&colorHasBeenSet](bool shouldSet, ImVec4 color)
+            {
+                if (colorHasBeenSet)
+                {
+                    ImGui::PopStyleColor();
+                    colorHasBeenSet = false;
+                }
+
+                if (shouldSet)
+                {
+                    ImGui::PushStyleColor(ImGuiCol_Text, color);
+                    colorHasBeenSet = true;
+                }
+            };
+            auto highlightTextFailedOrWarning = [&](bool isFailed, bool isWarning)
+            {
+                if (colorHasBeenSet)
+                {
+                    ImGui::PopStyleColor();
+                    colorHasBeenSet = false;
+                }
+
+                if (isFailed)
+                {
+                    ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
+                    colorHasBeenSet = true;
+                }
+                else if (isWarning)
+                {
+                    ImGui::PushStyleColor(ImGuiCol_Text, HighlightWarning);
+                    colorHasBeenSet = true;
+                }
+            };
+            auto resetTextHighlight = [&colorHasBeenSet]()
+            {
+                if (colorHasBeenSet)
+                {
+                    ImGui::PopStyleColor();
+                    colorHasBeenSet = false;
+                }
+            };
+
+            auto seeConsole = [](uint32_t issueCount, const char* searchString)
+            {
+                if (issueCount == 0)
+                {
+                    return AZStd::string{};
+                }
+                else
+                {
+                    return AZStd::string::format("(See \"%s\" messages in console output)", searchString);
+                }
+            };
+
+            auto seeBelow = [](uint32_t issueCount)
+            {
+                if (issueCount == 0)
+                {
+                    return AZStd::string{};
+                }
+                else
+                {
+                    return AZStd::string::format("(See below)");
+                }
+            };
+
+            uint32_t totalAsserts = 0;
+            uint32_t totalErrors = 0;
+            uint32_t totalWarnings = 0;
+            uint32_t totalScreenshotsCount = 0;
+            uint32_t totalScreenshotsFailed = 0;
+            uint32_t totalScreenshotWarnings = 0;
+            for (ScriptReport& scriptReport : m_scriptReports)
+            {
+                totalAsserts += scriptReport.m_assertCount;
+
+                // We don't include screenshot errors and warnings in these totals because those have their own line-items.
+                totalErrors += scriptReport.m_generalErrorCount;
+                totalWarnings += scriptReport.m_generalWarningCount;
+
+                totalScreenshotWarnings += scriptReport.m_screenshotWarningCount;
+                totalScreenshotsFailed += scriptReport.m_screenshotErrorCount;
+
+                // This will catch any false-negatives that could occur if the screenshot failure error messages change without also updating ScriptReport::OnPreError()
+                for (ScreenshotTestInfo& screenshotTest : scriptReport.m_screenshotTests)
+                {
+                    if (screenshotTest.m_officialComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass &&
+                        screenshotTest.m_officialComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::None)
+                    {
+                        AZ_Assert(scriptReport.m_screenshotErrorCount > 0, "If screenshot comparison failed in any way, m_screenshotErrorCount should be non-zero.");
+                    }
+                }
+            }
+
+            ImGui::Separator();
+
+            if (HasActiveScript())
+            {
+                ImGui::PushStyleColor(ImGuiCol_Text, HighlightWarning);
+                ImGui::Text("Script is running... (_ _)zzz");
+                ImGui::PopStyleColor();
+            }
+            else if (totalErrors > 0 || totalAsserts > 0 || totalScreenshotsFailed > 0)
+            {
+                ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
+                ImGui::Text("(>_<)  FAILED  (>_<)");
+                ImGui::PopStyleColor();
+            }
+            else
+            {
+                if (m_invalidationMessage.empty())
+                {
+                    ImGui::PushStyleColor(ImGuiCol_Text, HighlightPassed);
+                    ImGui::Text("\\(^_^)/  PASSED  \\(^_^)/");
+                    ImGui::PopStyleColor();
+                }
+                else
+                {
+                    ImGui::Text("(-_-) INVALID ... but passed (-_-)");
+                }
+            }
+
+            if (!m_invalidationMessage.empty())
+            {
+                ImGui::Separator();
+                ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
+                ImGui::Text("(%s)", m_invalidationMessage.c_str());
+                ImGui::PopStyleColor();
+            }
+
+            ImGui::Separator();
+
+            ImGui::Text("Test Script Count: %zu", m_scriptReports.size());
+
+            highlightTextIf(totalAsserts > 0, HighlightFailed);
+            ImGui::Text("Total Asserts:  %u %s", totalAsserts, seeConsole(totalAsserts, "Trace::Assert").c_str());
+
+            highlightTextIf(totalErrors > 0, HighlightFailed);
+            ImGui::Text("Total Errors:   %u %s", totalErrors, seeConsole(totalErrors, "Trace::Error").c_str());
+
+            highlightTextIf(totalWarnings > 0, HighlightWarning);
+            ImGui::Text("Total Warnings: %u %s", totalWarnings, seeConsole(totalWarnings, "Trace::Warning").c_str());
+
+            resetTextHighlight();
+            ImGui::Text("Total Screenshot Count: %u", totalScreenshotsCount);
+
+            highlightTextIf(totalScreenshotsFailed > 0, HighlightFailed);
+            ImGui::Text("Total Screenshot Failures: %u %s", totalScreenshotsFailed, seeBelow(totalScreenshotsFailed).c_str());
+
+            highlightTextIf(totalScreenshotWarnings > 0, HighlightWarning);
+            ImGui::Text("Total Screenshot Warnings: %u %s", totalScreenshotWarnings, seeBelow(totalScreenshotWarnings).c_str());
+
+            resetTextHighlight();
+
+            if (ImGui::Button("Update All Local Baseline Images"))
+            {
+                m_messageBox.OpenPopupConfirmation(
+                    "Update All Local Baseline Images",
+                    "This will replace all local baseline images \n"
+                    "with the images captured during this test run. \n"
+                    "Are you sure?",
+                    [this]() {
+                        UpdateAllLocalBaselineImages();
+                    });
+            }
+
+            int displayOption = m_displayOption;
+            ImGui::Combo("Display", &displayOption, DiplayOptions, AZ_ARRAY_SIZE(DiplayOptions));
+            m_displayOption = (DisplayOption)displayOption;
+
+            bool showWarnings = (m_displayOption == DisplayOption::AllResults) || (m_displayOption == DisplayOption::WarningsAndErrors);
+            bool showAll = (m_displayOption == DisplayOption::AllResults);
+
+            ImGui::Separator();
+
+            const ImGuiTreeNodeFlags FlagDefaultOpen = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_DefaultOpen;
+            const ImGuiTreeNodeFlags FlagDefaultClosed = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
+
+            for (ScriptReport& scriptReport : m_scriptReports)
+            {
+                const bool scriptPassed = scriptReport.m_assertCount == 0 && scriptReport.m_generalErrorCount == 0 && scriptReport.m_screenshotErrorCount == 0;
+                const bool scriptHasWarnings = scriptReport.m_generalWarningCount > 0 || scriptReport.m_screenshotWarningCount > 0;
+
+                // Skip if tests passed without warnings and we don't want to show successes
+                bool skipReport = (scriptPassed && !scriptHasWarnings && !showAll);
+
+                // Skip if we only have warnings only and we don't want to show warnings
+                skipReport = skipReport || (scriptPassed && scriptHasWarnings && !showWarnings);
+
+                if (skipReport)
+                {
+                    continue;
+                }
+
+                ImGuiTreeNodeFlags scriptNodeFlag = scriptPassed ? FlagDefaultClosed : FlagDefaultOpen;
+
+                AZStd::string header = AZStd::string::format("%s %s",
+                    scriptPassed ? "PASSED" : "FAILED",
+                    scriptReport.m_scriptAssetPath.c_str()
+                );
+
+                highlightTextFailedOrWarning(!scriptPassed, scriptHasWarnings);
+
+                if (ImGui::TreeNodeEx(&scriptReport, scriptNodeFlag, "%s", header.c_str()))
+                {
+                    resetTextHighlight();
+
+                    // Number of Asserts
+                    highlightTextIf(scriptReport.m_assertCount > 0, HighlightFailed);
+                    if (showAll || scriptReport.m_assertCount > 0)
+                    {
+                        ImGui::Text("Asserts:  %u %s", scriptReport.m_assertCount, seeConsole(scriptReport.m_assertCount, "Trace::Assert").c_str());
+                    }
+
+                    // Number of Errors
+                    highlightTextIf(scriptReport.m_generalErrorCount > 0, HighlightFailed);
+                    if (showAll || scriptReport.m_generalErrorCount > 0)
+                    {
+                        ImGui::Text("Errors:   %u %s", scriptReport.m_generalErrorCount, seeConsole(scriptReport.m_generalErrorCount, "Trace::Error").c_str());
+                    }
+
+                    // Number of Warnings
+                    highlightTextIf(scriptReport.m_generalWarningCount > 0, HighlightWarning);
+                    if (showAll || (showWarnings && scriptReport.m_generalWarningCount > 0))
+                    {
+                        ImGui::Text("Warnings: %u %s", scriptReport.m_generalWarningCount, seeConsole(scriptReport.m_generalWarningCount, "Trace::Warning").c_str());
+                    }
+
+                    resetTextHighlight();
+
+                    // Number of screenshots
+                    if (showAll || scriptReport.m_screenshotErrorCount > 0 || (showWarnings && scriptReport.m_screenshotWarningCount > 0))
+                    {
+                        ImGui::Text("Screenshot Test Count: %zu", scriptReport.m_screenshotTests.size());
+                    }
+
+                    // Number of screenshot failures
+                    highlightTextIf(scriptReport.m_screenshotErrorCount > 0, HighlightFailed);
+                    if (showAll || scriptReport.m_screenshotErrorCount > 0)
+                    {
+                        ImGui::Text("Screenshot Tests Failed: %u %s", scriptReport.m_screenshotErrorCount, seeBelow(scriptReport.m_screenshotErrorCount).c_str());
+                    }
+
+                    // Number of screenshot warnings
+                    highlightTextIf(scriptReport.m_screenshotWarningCount > 0, HighlightWarning);
+                    if (showAll || (showWarnings && scriptReport.m_screenshotWarningCount > 0))
+                    {
+                        ImGui::Text("Screenshot Warnings:     %u %s", scriptReport.m_screenshotWarningCount, seeBelow(scriptReport.m_screenshotWarningCount).c_str());
+                    }
+
+                    resetTextHighlight();
+
+                    for (ScreenshotTestInfo& screenshotResult : scriptReport.m_screenshotTests)
+                    {
+                        const bool screenshotPassed = screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass;
+                        const bool localBaselineWarning = screenshotResult.m_localComparisonResult.m_resultCode != ImageComparisonResult::ResultCode::Pass;
+
+                        // Skip if tests passed without warnings and we don't want to show successes
+                        bool skipScreenshot = (screenshotPassed && !localBaselineWarning && !showAll);
+
+                        // Skip if we only have warnings only and we don't want to show warnings
+                        skipScreenshot = skipScreenshot || (screenshotPassed && localBaselineWarning && !showWarnings);
+
+                        if (skipScreenshot)
+                        {
+                            continue;
+                        }
+
+                        AZStd::string fileName;
+                        AzFramework::StringFunc::Path::GetFullFileName(screenshotResult.m_screenshotFilePath.c_str(), fileName);
+
+                        AZStd::string headerSummary;
+                        if (!screenshotPassed)
+                        {
+                            headerSummary = "(" + screenshotResult.m_officialComparisonResult.GetSummaryString() + ") ";
+                        }
+                        if (localBaselineWarning)
+                        {
+                            headerSummary += "(Local Baseline Warning)";
+                        }
+
+                        ImGuiTreeNodeFlags screenshotNodeFlag = FlagDefaultClosed;
+                        AZStd::string screenshotHeader = AZStd::string::format("%s %s %s", screenshotPassed ? "PASSED" : "FAILED", fileName.c_str(), headerSummary.c_str());
+
+                        highlightTextFailedOrWarning(!screenshotPassed, localBaselineWarning);
+                        if (ImGui::TreeNodeEx(&screenshotResult, screenshotNodeFlag, "%s", screenshotHeader.c_str()))
+                        {
+                            resetTextHighlight();
+
+                            ImGui::Text(("Screenshot:        " + screenshotResult.m_screenshotFilePath).c_str());
+
+                            ImGui::Spacing();
+
+                            highlightTextIf(!screenshotPassed, HighlightFailed);
+
+                            ImGui::Text(("Official Baseline: " + screenshotResult.m_officialBaselineScreenshotFilePath).c_str());
+
+                            // Official Baseline Result
+                            ImGui::Indent();
+                            {
+                                ImGui::Text(screenshotResult.m_officialComparisonResult.GetSummaryString().c_str());
+
+                                if (screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::ThresholdExceeded ||
+                                    screenshotResult.m_officialComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::Pass)
+                                {
+                                    ImGui::Text("Used Tolerance: %s", screenshotResult.m_toleranceLevel.ToString().c_str());
+
+                                    const ImageComparisonToleranceLevel* suggestedTolerance = ScriptReporter::FindBestToleranceLevel(
+                                        screenshotResult.m_officialComparisonResult.m_finalDiffScore,
+                                        screenshotResult.m_toleranceLevel.m_filterImperceptibleDiffs);
+
+                                    if(suggestedTolerance)
+                                    {
+                                        ImGui::Text("Suggested Tolerance: %s", suggestedTolerance->ToString().c_str());
+                                    }
+
+                                    if (screenshotResult.m_toleranceLevel.m_filterImperceptibleDiffs)
+                                    {
+                                        // This gives an idea of what the tolerance level would be if the imperceptible diffs were not filtered out.
+                                        const ImageComparisonToleranceLevel* unfilteredTolerance = ScriptReporter::FindBestToleranceLevel(
+                                            screenshotResult.m_officialComparisonResult.m_standardDiffScore, false);
+
+                                        ImGui::Text("(Unfiltered Diff Score: %f%s)",
+                                            screenshotResult.m_officialComparisonResult.m_standardDiffScore,
+                                            unfilteredTolerance ? AZStd::string::format(" ~ '%s'", unfilteredTolerance->m_name.c_str()).c_str() : "");
+                                    }
+                                }
+
+                                resetTextHighlight();
+
+                                ImGui::PushID("Official");
+                                ShowDiffButton("View Diff", screenshotResult.m_officialBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
+                                ImGui::PopID();
+
+                                if (!screenshotPassed && ImGui::Button("Update"))
+                                {
+                                    if (screenshotResult.m_localComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::FileNotFound)
+                                    {
+                                        UpdateSourceBaselineImage(screenshotResult, true);
+                                    }
+                                    else
+                                    {
+                                        m_messageBox.OpenPopupConfirmation(
+                                            "Update Official Baseline Image",
+                                            "This will replace the official baseline image \n"
+                                            "with the image captured during this test run. \n"
+                                            "Are you sure?",
+                                            // It's important to bind screenshotResult by reference because UpdateOfficialBaselineImage will update it
+                                            [this, &screenshotResult]() {
+                                                UpdateSourceBaselineImage(screenshotResult, true);
+                                            });
+                                    }
+                                }
+                            }
+                            ImGui::Unindent();
+
+                            ImGui::Spacing();
+
+                            highlightTextIf(localBaselineWarning, HighlightWarning);
+
+                            ImGui::Text(("Local Baseline:    " + screenshotResult.m_localBaselineScreenshotFilePath).c_str());
+
+                            // Local Baseline Result
+                            ImGui::Indent();
+                            {
+                                ImGui::Text(screenshotResult.m_localComparisonResult.GetSummaryString().c_str());
+
+                                resetTextHighlight();
+
+                                ImGui::PushID("Local");
+                                ShowDiffButton("View Diff", screenshotResult.m_localBaselineScreenshotFilePath, screenshotResult.m_screenshotFilePath);
+                                ImGui::PopID();
+
+                                if (localBaselineWarning && ImGui::Button("Update"))
+                                {
+                                    if (screenshotResult.m_localComparisonResult.m_resultCode == ImageComparisonResult::ResultCode::FileNotFound)
+                                    {
+                                        UpdateLocalBaselineImage(screenshotResult, true);
+                                    }
+                                    else
+                                    {
+                                        m_messageBox.OpenPopupConfirmation(
+                                            "Update Local Baseline Image",
+                                            "This will replace the local baseline image \n"
+                                            "with the image captured during this test run. \n"
+                                            "Are you sure?",
+                                            // It's important to bind screenshotResult by reference because UpdateLocalBaselineImage will update it
+                                            [this, &screenshotResult]() {
+                                                UpdateLocalBaselineImage(screenshotResult, true);
+                                            });
+                                    }
+                                }
+                            }
+                            ImGui::Unindent();
+
+                            ImGui::Spacing();
+
+                            resetTextHighlight();
+
+                            ImGui::TreePop();
+                        }
+                    }
+
+                    ImGui::TreePop();
+                }
+
+                resetTextHighlight();
+            }
+
+            resetTextHighlight();
+
+            // Repeat the m_invalidationMessage at the bottom as well, to make sure the user doesn't miss it.
+            if (!m_invalidationMessage.empty())
+            {
+                ImGui::Separator();
+                ImGui::PushStyleColor(ImGuiCol_Text, HighlightFailed);
+                ImGui::Text("(%s)", m_invalidationMessage.c_str());
+                ImGui::PopStyleColor();
+            }
+        }
+
+        m_messageBox.TickPopup();
+
+        ImGui::End();
+    }
+
+    void ScriptReporter::OpenReportDialog()
+    {
+        m_showReportDialog = true;
+    }
+
+    ScriptReporter::ScriptReport* ScriptReporter::GetCurrentScriptReport()
+    {
+        if (!m_currentScriptIndexStack.empty())
+        {
+            return &m_scriptReports[m_currentScriptIndexStack.back()];
+        }
+        else
+        {
+            return nullptr;
+        }
+    }
+
+    void ScriptReporter::ReportScriptError([[maybe_unused]] const AZStd::string& message)
+    {
+        AZ_Error("Automation", false, "Script: %s", message.c_str());
+    }
+
+    void ScriptReporter::ReportScriptWarning([[maybe_unused]] const AZStd::string& message)
+    {
+        AZ_Warning("Automation", false, "Script: %s", message.c_str());
+    }
+
+    void ScriptReporter::ReportScriptIssue(const AZStd::string& message, TraceLevel traceLevel)
+    {
+        switch (traceLevel)
+        {
+        case TraceLevel::Error:
+            ReportScriptError(message);
+            break;
+        case TraceLevel::Warning:
+            ReportScriptWarning(message);
+            break;
+        default:
+            AZ_Assert(false, "Unhandled TraceLevel");
+        }
+    }
+
+    void ScriptReporter::ReportScreenshotComparisonIssue(const AZStd::string& message, const AZStd::string& expectedImageFilePath, const AZStd::string& actualImageFilePath, TraceLevel traceLevel)
+    {
+        AZStd::string fullMessage = AZStd::string::format("%s\n    Expected: '%s'\n    Actual:   '%s'",
+            message.c_str(),
+            expectedImageFilePath.c_str(),
+            actualImageFilePath.c_str());
+
+        ReportScriptIssue(fullMessage, traceLevel);
+    }
+
+    bool ScriptReporter::LoadPpmData(ImageComparisonResult& imageComparisonResult, const AZStd::string& path, AZStd::vector<uint8_t>& buffer, AZ::RHI::Size& size, AZ::RHI::Format& format, TraceLevel traceLevel)
+    {
+        const size_t maxFileSize = 1024 * 1024 * 25;
+
+        auto readScreenshotFileResult = AZ::Utils::ReadFile<AZStd::vector<uint8_t>>(path, maxFileSize);
+        if (!readScreenshotFileResult.IsSuccess())
+        {
+            ReportScriptIssue(AZStd::string::format("Screenshot check failed. %s", readScreenshotFileResult.GetError().c_str()), traceLevel);
+            imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
+            return false;
+        }
+
+        if (!AZ::Utils::PpmFile::CreateImageBufferFromPpm(readScreenshotFileResult.GetValue(), buffer, size, format))
+        {
+            ReportScriptIssue(AZStd::string::format("Screenshot check failed. Failed to read file '%s'", path.c_str()), traceLevel);
+            imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotLoaded;
+            return false;
+        }
+
+        return true;
+    }
+
+    bool ScriptReporter::DiffImages(ImageComparisonResult& imageComparisonResult, const AZStd::string& expectedImageFilePath, const AZStd::string& actualImageFilePath, TraceLevel traceLevel)
+    {
+        using namespace AZ::Utils;
+
+        AZStd::vector<uint8_t> actualImageBuffer;
+        AZ::RHI::Size actualImageSize;
+        AZ::RHI::Format actualImageFormat;
+        if (!LoadPpmData(imageComparisonResult, actualImageFilePath, actualImageBuffer, actualImageSize, actualImageFormat, traceLevel))
+        {
+            return false;
+        }
+
+        AZStd::vector<uint8_t> expectedImageBuffer;
+        AZ::RHI::Size expectedImageSize;
+        AZ::RHI::Format expectedImageFormat;
+        if (!LoadPpmData(imageComparisonResult, expectedImageFilePath, expectedImageBuffer, expectedImageSize, expectedImageFormat, traceLevel))
+        {
+            return false;
+        }
+
+        float diffScore = 0.0f;
+        float filteredDiffScore = 0.0f;
+
+        static constexpr float ImperceptibleDiffFilter = 0.01;
+
+        ImageDiffResultCode rmsResult = AZ::Utils::CalcImageDiffRms(
+            actualImageBuffer, actualImageSize, actualImageFormat,
+            expectedImageBuffer, expectedImageSize, expectedImageFormat,
+            &diffScore,
+            &filteredDiffScore,
+            ImperceptibleDiffFilter);
+
+        if (rmsResult != ImageDiffResultCode::Success)
+        {
+            if(rmsResult == ImageDiffResultCode::SizeMismatch)
+            {
+                ReportScreenshotComparisonIssue(AZStd::string::format("Screenshot check failed. Sizes don't match. Expected %u x %u but was %u x %u.",
+                    expectedImageSize.m_width, expectedImageSize.m_height,
+                    actualImageSize.m_width, actualImageSize.m_height),
+                    expectedImageFilePath,
+                    actualImageFilePath,
+                    traceLevel);
+                imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::WrongSize;
+                return false;
+            }
+            else if (rmsResult == ImageDiffResultCode::FormatMismatch || rmsResult == ImageDiffResultCode::UnsupportedFormat)
+            {
+                ReportScreenshotComparisonIssue(AZStd::string::format("Screenshot check failed. Could not compare screenshots due to a format issue."),
+                    expectedImageFilePath,
+                    actualImageFilePath,
+                    traceLevel);
+                imageComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::WrongFormat;
+                return false;
+            }
+        }
+
+        imageComparisonResult.m_standardDiffScore = diffScore;
+        imageComparisonResult.m_filteredDiffScore = filteredDiffScore;
+        imageComparisonResult.m_finalDiffScore = diffScore; // Set the final score to the standard score just in case the filtered one is ignored
+        return true;
+    }
+
+    void ScriptReporter::UpdateAllLocalBaselineImages()
+    {
+        int failureCount = 0;
+        int successCount = 0;
+
+        for (ScriptReport& report : m_scriptReports)
+        {
+            for (ScreenshotTestInfo& screenshotTest : report.m_screenshotTests)
+            {
+                if (UpdateLocalBaselineImage(screenshotTest, false))
+                {
+                    successCount++;
+                }
+                else
+                {
+                    failureCount++;
+                }
+            }
+        }
+
+        ShowUpdateLocalBaselineResult(successCount, failureCount);
+    }
+    
+    bool ScriptReporter::UpdateLocalBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog)
+    {
+        const AZStd::string destinationFile = ScreenshotPaths::GetLocalBaseline(screenshotTest.m_screenshotFilePath);
+
+        AZStd::string destinationFolder = destinationFile;
+        AzFramework::StringFunc::Path::StripFullName(destinationFolder);
+
+        m_fileIoErrorHandler.BusConnect();
+
+        bool failed = false;
+
+        if (!AZ::IO::LocalFileIO::GetInstance()->CreatePath(destinationFolder.c_str()))
+        {
+            failed = true;
+            m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to create folder '%s'.", destinationFolder.c_str()));
+        }
+
+        if (!AZ::IO::LocalFileIO::GetInstance()->Copy(screenshotTest.m_screenshotFilePath.c_str(), destinationFile.c_str()))
+        {
+            failed = true;
+            m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to copy '%s' to '%s'.", screenshotTest.m_screenshotFilePath.c_str(), destinationFile.c_str()));
+        }
+
+        m_fileIoErrorHandler.BusDisconnect();
+
+        if (!failed)
+        {
+            // Since we just replaced the baseline image, we can update this screenshot test result as an exact match.
+            // This will update the ImGui report dialog by the next frame.
+            ClearImageComparisonResult(screenshotTest.m_localComparisonResult);
+        }
+
+        if (showResultDialog)
+        {
+            int successCount = !failed;
+            int failureCount = failed;
+            ShowUpdateLocalBaselineResult(successCount, failureCount);
+        }
+
+        return !failed;
+    }
+
+    bool ScriptReporter::UpdateSourceBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog)
+    {
+        bool success = true;
+        auto io = AZ::IO::LocalFileIO::GetInstance();
+
+        // Get source folder
+        if (m_officialBaselineSourceFolder.empty())
+        {
+            AZStd::string appRoot;
+            AzFramework::ApplicationRequests::Bus::BroadcastResult(appRoot, &AzFramework::ApplicationRequests::GetAppRoot);
+            AzFramework::StringFunc::Path::Join(appRoot.c_str(), "AtomSampleViewer/Scripts/ExpectedScreenshots", m_officialBaselineSourceFolder);
+
+            if (!io->Exists(m_officialBaselineSourceFolder.c_str()))
+            {
+                AZ_Error("Automation", false, "Could not find source folder '%s'. Copying to source baseline can only be used on dev platforms.", m_officialBaselineSourceFolder.c_str());
+                m_officialBaselineSourceFolder.clear();
+                success = false;
+            }
+        }
+
+        // Get official cache baseline file
+        const AZStd::string cacheFilePath = ScreenshotPaths::GetOfficialBaseline(screenshotTest.m_screenshotFilePath);
+
+        // Divide cache file path into components to we can access the file name and the parent folder
+        AZStd::fixed_vector<AZ::IO::FixedMaxPathString, 16> reversePathComponents;
+        auto GatherPathSegments = [&reversePathComponents](AZStd::string_view token)
+        {
+            reversePathComponents.emplace_back(token);
+        };
+        AzFramework::StringFunc::TokenizeVisitorReverse(cacheFilePath, GatherPathSegments, "/\\");
+
+        // Source folder path
+        // ".../AtomSampleViewer/Scripts/ExpectedScreenshots/" + "MyTestFolder/"
+        AZStd::string sourceFolderPath = AZStd::string::format("%s\\%s", m_officialBaselineSourceFolder.c_str(), reversePathComponents[1].c_str());
+
+        // Source file path
+        // ".../AtomSampleViewer/Scripts/ExpectedScreenshots/MyTestFolder/" + "MyTest.ppm"
+        AZStd::string sourceFilePath = AZStd::string::format("%s\\%s", sourceFolderPath.c_str(), reversePathComponents[0].c_str());
+
+        m_fileIoErrorHandler.BusConnect();
+
+        // Create parent folder if it doesn't exist
+        if (success && !io->CreatePath(sourceFolderPath.c_str()))
+        {
+            success = false;
+            m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to create folder '%s'.", sourceFolderPath.c_str()));
+        }
+
+        // Replace source screenshot with new result
+        if (success && !io->Copy(screenshotTest.m_screenshotFilePath.c_str(), sourceFilePath.c_str()))
+        {
+            success = false;
+            m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to copy '%s' to '%s'.", screenshotTest.m_screenshotFilePath.c_str(), sourceFilePath.c_str()));
+        }
+
+        m_fileIoErrorHandler.BusDisconnect();
+
+        if (success)
+        {
+            // Since we just replaced the baseline image, we can update this screenshot test result as an exact match.
+            // This will update the ImGui report dialog by the next frame.
+            ClearImageComparisonResult(screenshotTest.m_officialComparisonResult);
+        }
+
+        if (showResultDialog)
+        {
+            AZStd::string message = "Destination: " + sourceFilePath + "\n";
+            message += success
+                ? AZStd::string::format("Copy successful!.\n")
+                : AZStd::string::format("Copy failed!\n");
+
+            m_messageBox.OpenPopupMessage("Update Baseline Image(s) Result", message);
+        }
+
+        return success;
+    }
+
+    void ScriptReporter::ClearImageComparisonResult(ImageComparisonResult& comparisonResult)
+    {
+        comparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
+        comparisonResult.m_standardDiffScore = 0.0f;
+        comparisonResult.m_filteredDiffScore = 0.0f;
+        comparisonResult.m_finalDiffScore = 0.0f;
+    }
+
+    void ScriptReporter::ShowUpdateLocalBaselineResult(int successCount, int failureCount)
+    {
+        AZStd::string message;
+        if (failureCount == 0 && successCount == 0)
+        {
+            message = "No screenshots found.";
+        }
+        else
+        {
+            message = "Destination: " + ScreenshotPaths::GetLocalBaselineFolder(true) + "\n";
+
+            if (successCount > 0)
+            {
+                message += AZStd::string::format("Successfully copied %d files.\n", successCount);
+            }
+            if (failureCount > 0)
+            {
+                message += AZStd::string::format("Failed to copy %d files.\n", failureCount);
+            }
+        }
+
+        m_messageBox.OpenPopupMessage("Update Baseline Image(s) Result", message);
+    }
+
+    void ScriptReporter::CheckLatestScreenshot(const ImageComparisonToleranceLevel* toleranceLevel)
+    {
+        AZ_Assert(GetCurrentScriptReport(), "There is no active script");
+
+        if (GetCurrentScriptReport() == nullptr || GetCurrentScriptReport()->m_screenshotTests.empty())
+        {
+            ReportScriptError("CheckLatestScreenshot() did not find any screenshots to check.");
+            return;
+        }
+
+        ScreenshotTestInfo& screenshotTestInfo = GetCurrentScriptReport()->m_screenshotTests.back();
+
+        if (toleranceLevel == nullptr)
+        {
+            screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::NullImageComparisonToleranceLevel;
+            ReportScriptError("Screenshot check failed. No ImageComparisonToleranceLevel provided.");
+            return;
+        }
+
+        screenshotTestInfo.m_toleranceLevel = *toleranceLevel;
+
+        screenshotTestInfo.m_officialBaselineScreenshotFilePath = ScreenshotPaths::GetOfficialBaseline(screenshotTestInfo.m_screenshotFilePath);
+        if (screenshotTestInfo.m_officialBaselineScreenshotFilePath.empty())
+        {
+            ReportScriptError(AZStd::string::format("Screenshot check failed. Could not determine expected screenshot path for '%s'", screenshotTestInfo.m_screenshotFilePath.c_str()));
+            screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
+        }
+        else
+        {
+            bool imagesWereCompared = DiffImages(
+                screenshotTestInfo.m_officialComparisonResult,
+                screenshotTestInfo.m_officialBaselineScreenshotFilePath,
+                screenshotTestInfo.m_screenshotFilePath,
+                TraceLevel::Error);
+
+            if (imagesWereCompared)
+            {
+                screenshotTestInfo.m_officialComparisonResult.m_finalDiffScore = toleranceLevel->m_filterImperceptibleDiffs ?
+                    screenshotTestInfo.m_officialComparisonResult.m_filteredDiffScore :
+                    screenshotTestInfo.m_officialComparisonResult.m_standardDiffScore;
+
+                if (screenshotTestInfo.m_officialComparisonResult.m_finalDiffScore <= toleranceLevel->m_threshold)
+                {
+                    screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
+                }
+                else
+                {
+                    ReportScreenshotComparisonIssue(
+                        AZStd::string::format("Screenshot check failed. Diff score %f exceeds threshold of %f ('%s').",
+                            screenshotTestInfo.m_officialComparisonResult.m_finalDiffScore, toleranceLevel->m_threshold, toleranceLevel->m_name.c_str()),
+                        screenshotTestInfo.m_officialBaselineScreenshotFilePath,
+                        screenshotTestInfo.m_screenshotFilePath,
+                        TraceLevel::Error);
+                    screenshotTestInfo.m_officialComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::ThresholdExceeded;
+                }
+            }
+        }
+
+        screenshotTestInfo.m_localBaselineScreenshotFilePath = ScreenshotPaths::GetLocalBaseline(screenshotTestInfo.m_screenshotFilePath);
+        if (screenshotTestInfo.m_localBaselineScreenshotFilePath.empty())
+        {
+            ReportScriptWarning(AZStd::string::format("Screenshot check failed. Could not determine local baseline screenshot path for '%s'", screenshotTestInfo.m_screenshotFilePath.c_str()));
+            screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::FileNotFound;
+        }
+        else
+        {
+            // Local screenshots should be expected match 100% every time, otherwise warnings are reported. This will help developers track and investigate changes,
+            // for example if they make local changes that impact some unrelated AtomSampleViewer sample in an unexpected way, they will see a warning about this.
+
+            bool imagesWereCompared = DiffImages(
+                screenshotTestInfo.m_localComparisonResult,
+                screenshotTestInfo.m_localBaselineScreenshotFilePath,
+                screenshotTestInfo.m_screenshotFilePath,
+                TraceLevel::Warning);
+
+            if (imagesWereCompared)
+            {
+                if(screenshotTestInfo.m_localComparisonResult.m_standardDiffScore == 0.0f)
+                {
+                    screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::Pass;
+                }
+                else
+                {
+                    ReportScreenshotComparisonIssue(
+                        AZStd::string::format("Screenshot check failed. Screenshot does not match the local baseline; something has changed. Diff score is %f.", screenshotTestInfo.m_localComparisonResult.m_standardDiffScore),
+                        screenshotTestInfo.m_localBaselineScreenshotFilePath,
+                        screenshotTestInfo.m_screenshotFilePath,
+                        TraceLevel::Warning);
+                    screenshotTestInfo.m_localComparisonResult.m_resultCode = ImageComparisonResult::ResultCode::ThresholdExceeded;
+                }
+            }
+        }
+
+    }
+} // namespace AtomSampleViewer

+ 254 - 0
Gem/Code/Source/Automation/ScriptReporter.h

@@ -0,0 +1,254 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/Debug/TraceMessageBus.h>
+#include <AzFramework/StringFunc/StringFunc.h>
+#include <Atom/Utils/ImageComparison.h>
+#include <Automation/ImageComparisonConfig.h>
+#include <Utils/ImGuiMessageBox.h>
+#include <Utils/FileIOErrorHandler.h>
+
+namespace AtomSampleViewer
+{
+    struct ImageComparisonToleranceLevel;
+
+    namespace ScreenshotPaths
+    {
+        //! Returns the path to the screenshots capture folder.
+        //! @resolvePath indicates whether to call ResolvePath() which will produce a full path, or keep the shorter asset folder path.
+        AZStd::string GetScreenshotsFolder(bool resolvePath);
+
+        //! Returns the path to the local baseline folder, which stores copies of screenshots previously taken on this machine.
+        //! @resolvePath indicates whether to call ResolvePath() which will produce a full path, or keep the shorter asset folder path.
+        AZStd::string GetLocalBaselineFolder(bool resolvePath);
+
+        //! Returns the path to the official baseline folder, which stores copies of expected screenshots saved in source control.
+        //! @resolvePath indicates whether to call ResolvePath() which will produce a full path, or keep the shorter asset folder path.
+        AZStd::string GetOfficialBaselineFolder(bool resolvePath);
+
+        //! Returns the path to the local baseline image that corresponds to @forScreenshotFile
+        AZStd::string GetLocalBaseline(const AZStd::string& forScreenshotFile);
+
+        //! Returns the path to the official baseline image that corresponds to @forScreenshotFile
+        AZStd::string GetOfficialBaseline(const AZStd::string& forScreenshotFile);
+    }
+
+    //! Collects data about each script run by the ScriptManager.
+    //! This includes counting errors, checking screenshots, and providing a final report dialog.
+    class ScriptReporter
+    {
+    public:
+
+        //! Set the list of available tolerance levels, so the report can suggest an alternate level that matches the actual results.
+        void SetAvailableToleranceLevels(const AZStd::vector<ImageComparisonToleranceLevel>& toleranceLevels);
+
+        //! Clears all recorded data.
+        void Reset();
+
+        //! Invalidates the final results when displaying a report to the user. This can be used to highlight
+        //! local changes that were made, and remind the user that these results should not be considered official.
+        //! Use an empty string to clear the invalidation.
+        void SetInvalidationMessage(const AZStd::string& message);
+
+        //! Indicates that a new script has started processing.
+        //! Any subsequent errors will be included as part of this script's report.
+        void PushScript(const AZStd::string& scriptAssetPath);
+
+        //! Indicates that the current script has finished executing.
+        //! Any subsequent errors will be included as part of the prior script's report.
+        void PopScript();
+
+        //! Returns whether there are active processing scripts (i.e. more PushScript() calls than PopScript() calls)
+        bool HasActiveScript() const;
+
+        //! Indicates that a new screenshot is about to be captured.
+        bool AddScreenshotTest(const AZStd::string& path);
+
+        //! Check the latest screenshot using default thresholds.
+        void CheckLatestScreenshot(const ImageComparisonToleranceLevel* comparisonPreset);
+
+        //! Opens the script report dialog.
+        //! This displays all the collected script reporting data, provides links to tools for analyzing data like
+        //! viewing screenshot diffs. It can be left open during processing and will update in real-time.
+        void OpenReportDialog();
+
+        //! Called every frame to update the ImGui dialog
+        void TickImGui();
+
+        //! Returns true if there are any errors or asserts in the script report
+        bool HasErrorsAssertsInReport() const;
+
+        struct ImageComparisonResult
+        {
+            enum class ResultCode
+            {
+                None,
+                Pass,
+                FileNotFound,
+                FileNotLoaded,
+                WrongSize,
+                WrongFormat,
+                NullImageComparisonToleranceLevel,
+                ThresholdExceeded
+            };
+
+            ResultCode m_resultCode = ResultCode::None;
+            float m_standardDiffScore = 0.0f;
+            float m_filteredDiffScore = 0.0f; //!< The diff score after filtering out visually imperceptible differences.
+            float m_finalDiffScore = 0.0f; //! The diff score that was used for comparison. May be m_diffScore or m_filteredDiffScore.
+
+            AZStd::string GetSummaryString() const;
+        };
+
+        //! Records all the information about a screenshot comparison test.
+        struct ScreenshotTestInfo
+        {
+            AZStd::string m_screenshotFilePath;
+            AZStd::string m_officialBaselineScreenshotFilePath; //!< The path to the official baseline image that is checked into source control
+            AZStd::string m_localBaselineScreenshotFilePath;    //!< The path to a local baseline image that was established by the user
+            ImageComparisonToleranceLevel m_toleranceLevel;     //!< Tolerance for checking against the official baseline image
+            ImageComparisonResult m_officialComparisonResult;   //!< Result of comparing against the official baseline image, for reporting test failure
+            ImageComparisonResult m_localComparisonResult;      //!< Result of comparing against a local baseline, for reporting warnings
+        };
+
+        //! Records all the information about a single test script.
+        struct ScriptReport : public AZ::Debug::TraceMessageBus::Handler
+        {
+            ~ScriptReport()
+            {
+                AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
+            }
+
+            bool OnPreAssert(const char* /*fileName*/, int /*line*/, const char* /*func*/, [[maybe_unused]] const char* message) override
+            {
+                ++m_assertCount;
+                return false;
+            }
+
+            bool OnPreError(const char* /*window*/, const char* /*fileName*/, int /*line*/, const char* /*func*/, const char* message) override
+            {
+                if (AZStd::string::npos == AzFramework::StringFunc::Find(message, "Screenshot check failed"))
+                {
+                    ++m_generalErrorCount;
+                }
+                else
+                {
+                    ++m_screenshotErrorCount;
+                }
+
+                return false;
+            }
+
+            bool OnPreWarning(const char* /*window*/, const char* /*fileName*/, int /*line*/, const char* /*func*/, const char* message) override
+            {
+                if (AZStd::string::npos == AzFramework::StringFunc::Find(message, "Screenshot does not match the local baseline"))
+                {
+                    ++m_generalWarningCount;
+                }
+                else
+                {
+                    ++m_screenshotWarningCount;
+                }
+
+                return false;
+            }
+
+            AZStd::string m_scriptAssetPath;
+
+            uint32_t m_assertCount = 0;
+
+            uint32_t m_generalErrorCount = 0;
+            uint32_t m_screenshotErrorCount = 0;
+
+            uint32_t m_generalWarningCount = 0;
+            uint32_t m_screenshotWarningCount = 0;
+
+            AZStd::vector<ScreenshotTestInfo> m_screenshotTests;
+        };
+        
+        const AZStd::vector<ScriptReport>& GetScriptReport() const { return m_scriptReports; }
+
+    private:
+
+        // Reports a script error using standard formatting that matches ScriptManager
+        enum class TraceLevel
+        {
+            Error,
+            Warning
+        };
+
+        // Controls which results are shown to the user
+        // Must match static const char* DiplayOptions in .cpp file
+        enum DisplayOption : int
+        {
+            AllResults,
+            WarningsAndErrors,
+            ErrorsOnly
+        };
+
+        static void ReportScriptError(const AZStd::string& message);
+        static void ReportScriptWarning(const AZStd::string& message);
+        static void ReportScriptIssue(const AZStd::string& message, TraceLevel traceLevel);
+        static void ReportScreenshotComparisonIssue(const AZStd::string& message, const AZStd::string& expectedImageFilePath, const AZStd::string& actualImageFilePath, TraceLevel traceLevel);
+
+        // Loads image data from a .ppm file.
+        // @param imageComparisonResult will be set to an error code if the function fails
+        // @param path the path the .ppm file
+        // @param buffer will be filled with the raw image data from the .ppm file
+        // @param size will be set to the image size of the .ppm file
+        // @param format will be set to the pixel format of the .ppm file
+        // @return true if the file was loaded successfully
+        static bool LoadPpmData(ImageComparisonResult& imageComparisonResult, const AZStd::string& path, AZStd::vector<uint8_t>& buffer, AZ::RHI::Size& size, AZ::RHI::Format& format, TraceLevel traceLevel);
+
+        // Compares two image files and updates the ImageComparisonResult accordingly.
+        // Returns false if an error prevented the comparison.
+        static bool DiffImages(ImageComparisonResult& imageComparisonResult, const AZStd::string& expectedImageFilePath, const AZStd::string& actualImageFilePath, TraceLevel traceLevel);
+
+        // Copies all captured screenshots to the local baseline folder. These can be used as an alternative to the central baseline for comparison.
+        void UpdateAllLocalBaselineImages();
+
+        // Copies a single captured screenshot to the local baseline folder. This can be used as an alternative to the central baseline for comparison.
+        bool UpdateLocalBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog);
+
+        // Copies a single captured screenshot to the official baseline source folder.
+        bool UpdateSourceBaselineImage(ScreenshotTestInfo& screenshotTest, bool showResultDialog);
+
+        // Clears comparison result to passing with no errors or warnings
+        void ClearImageComparisonResult(ImageComparisonResult& comparisonResult);
+
+        // Show a message box to let the user know the results of updating local baseline images
+        void ShowUpdateLocalBaselineResult(int successCount, int failureCount);
+
+        const ImageComparisonToleranceLevel* FindBestToleranceLevel(float diffScore, bool filterImperceptibleDiffs) const;
+
+        void ShowReportDialog();
+
+        void ShowDiffButton(const char* buttonLabel, const AZStd::string& imagePathA, const AZStd::string& imagePathB);
+
+        ScriptReport* GetCurrentScriptReport();
+
+        ImGuiMessageBox m_messageBox;
+        FileIOErrorHandler m_fileIoErrorHandler;
+
+        AZStd::vector<ImageComparisonToleranceLevel> m_availableToleranceLevels;
+        AZStd::string m_invalidationMessage;
+
+        AZStd::vector<ScriptReport> m_scriptReports; //< Tracks errors for the current active script
+        AZStd::vector<size_t> m_currentScriptIndexStack; //< Tracks which of the scripts in m_scriptReports is currently active
+        bool m_showReportDialog = false;
+        DisplayOption m_displayOption = DisplayOption::AllResults;
+        AZStd::string m_officialBaselineSourceFolder; //< Used for updating official baseline screenshots
+    };
+
+} // namespace AtomSampleViewer

+ 35 - 0
Gem/Code/Source/Automation/ScriptRunnerBus.h

@@ -0,0 +1,35 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/std/string/string_view.h>
+#include <AzCore/EBus/EBus.h>
+
+namespace AtomSampleViewer
+{
+    class ScriptRunnerRequests
+        : public AZ::EBusTraits
+    {
+    public:
+        static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
+
+        //! Can be used by sample components to temporarily pause script processing, for example
+        //! to delay until some required resources are loaded and initialized.
+        virtual void PauseScript() = 0;
+        virtual void PauseScriptWithTimeout(float timeout) = 0;
+        virtual void ResumeScript() = 0;
+    };
+
+    using ScriptRunnerRequestBus = AZ::EBus<ScriptRunnerRequests>;
+
+} // namespace AtomSampleViewer

+ 634 - 0
Gem/Code/Source/Automation/ScriptableImGui.cpp

@@ -0,0 +1,634 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRepeaterBus.h>
+#include <AzFramework/StringFunc/StringFunc.h>
+#include <Utils/Utils.h>
+
+namespace AtomSampleViewer
+{
+    ScriptableImGui* ScriptableImGui::s_instance = nullptr;
+
+    void ScriptableImGui::Create()
+    {
+        AZ_Assert(s_instance == nullptr, "instance already called");
+        s_instance = aznew ScriptableImGui();
+    }
+
+    void ScriptableImGui::Destory()
+    {
+        AZ_Assert(s_instance != nullptr, "instance is null");
+        delete s_instance;
+        s_instance = nullptr;
+    }
+
+    void ScriptableImGui::CheckAllActionsConsumed()
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+        AZ_Error("Automation", s_instance->m_scriptedActions.empty(), "Not all scripted ImGui actions were consumed");
+        for (auto iter : s_instance->m_scriptedActions)
+        {
+            AZ_Error("Automation", false, "Scripted action for '%s' not consumed", iter.first.c_str());
+        }
+
+        AZ_Error("Automation", s_instance->m_nameContextStack.empty(), "PushNameContext and PopNameContext calls didn't match");
+    }
+
+    void ScriptableImGui::ClearActions()
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+        s_instance->m_scriptedActions.clear();
+        s_instance->m_nameContextStack.clear();
+    }
+
+    void ScriptableImGui::PushNameContext(const AZStd::string& nameContext)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+        s_instance->m_nameContextStack.push_back(nameContext);
+    }
+
+    void ScriptableImGui::PopNameContext()
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+        AZ_Assert(!s_instance->m_nameContextStack.empty(), "Called PopNameContext too many times");
+        s_instance->m_nameContextStack.pop_back();
+    }
+
+    ScriptableImGui::ActionItem ScriptableImGui::FindAndRemoveAction(const AZStd::string& pathToImGuiItem)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+        auto iter = s_instance->m_scriptedActions.find(pathToImGuiItem);
+        if (iter != s_instance->m_scriptedActions.end())
+        {
+            ScriptableImGui::ActionItem item = AZStd::move(iter->second);
+            s_instance->m_scriptedActions.erase(iter);
+            return AZStd::move(item);
+        }
+
+        return ScriptableImGui::ActionItem{};
+    }
+
+    AZStd::string ScriptableImGui::MakeFullPath(const AZStd::string& forLabel)
+    {
+        static constexpr char Delimiter[] = "/";
+
+        AZStd::string fullPath;
+        if (!s_instance->m_nameContextStack.empty())
+        {
+            AzFramework::StringFunc::Join(fullPath, s_instance->m_nameContextStack.begin(), s_instance->m_nameContextStack.end(), Delimiter);
+            fullPath += Delimiter;
+        }
+        fullPath += forLabel;
+
+        return fullPath;
+    }
+
+    void ScriptableImGui::ReportScriptError([[maybe_unused]] const char* message)
+    {
+        AZ_Error("Automation", false, "Script: %s", message);
+    }
+
+    void ScriptableImGui::SetBool(const AZStd::string& pathToImGuiItem, bool value)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+        s_instance->m_scriptedActions[pathToImGuiItem] = value;
+    }
+
+    void ScriptableImGui::SetNumber(const AZStd::string& pathToImGuiItem, float value)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+        s_instance->m_scriptedActions[pathToImGuiItem] = value;
+    }
+
+    void ScriptableImGui::SetVector(const AZStd::string& pathToImGuiItem, const AZ::Vector2& value)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+        s_instance->m_scriptedActions[pathToImGuiItem] = value;
+    }
+
+    void ScriptableImGui::SetVector(const AZStd::string& pathToImGuiItem, const AZ::Vector3& value)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+        s_instance->m_scriptedActions[pathToImGuiItem] = value;
+    }
+
+    void ScriptableImGui::SetString(const AZStd::string& pathToImGuiItem, const AZStd::string& value)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+        s_instance->m_scriptedActions[pathToImGuiItem] = value;
+    }
+
+    template<typename ActionDataT>
+    bool ScriptableImGui::ActionHelper(
+        const char* label,
+        AZStd::function<bool()> imguiAction,
+        AZStd::function<void(const AZStd::string& /*pathToImGuiItem*/)> reportScriptableAction,
+        AZStd::function<bool(ActionDataT /*scriptArg*/)> handleScriptedAction,
+        bool shouldReportScriptableActionAfterAnyChange)
+    {
+        bool imResult = imguiAction();
+
+        const AZStd::string pathToImGuiItem = MakeFullPath(label);
+
+        if (ImGui::IsItemDeactivatedAfterEdit() || (shouldReportScriptableActionAfterAnyChange && imResult))
+        {
+            reportScriptableAction(pathToImGuiItem);
+        }
+
+        bool scriptResult = false;
+
+        ActionItem actionItem = FindAndRemoveAction(pathToImGuiItem);
+        bool foundAction = !AZStd::holds_alternative<InvalidActionItem>(actionItem);
+        if (foundAction)
+        {
+            if (AZStd::holds_alternative<ActionDataT>(actionItem))
+            {
+                scriptResult = handleScriptedAction(AZStd::get<ActionDataT>(actionItem));
+            }
+            else
+            {
+                ReportScriptError(AZStd::string::format("Wrong data type used to set '%s'", pathToImGuiItem.c_str()).c_str());
+            }
+        }
+
+        return imResult || scriptResult;
+    }
+
+    bool ScriptableImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags)
+    {
+        PushNameContext(name);
+        return ImGui::Begin(name, p_open, flags);
+    }
+
+    void ScriptableImGui::End()
+    {
+        ImGui::End();
+        PopNameContext();
+    }
+
+    bool ScriptableImGui::Checkbox(const char* label, bool* v)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::Checkbox(label, v);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', %s)", pathToImGuiItem.c_str(), (*v ? "true" : "false"));
+        };
+
+        auto handleScriptedAction = [&](bool scriptArg)
+        {
+            if (*v != scriptArg)
+            {
+                *v = scriptArg;
+                return true;
+            }
+            return false;
+        };
+
+        return ActionHelper<bool>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::Button(const char* label, const ImVec2& size_arg)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::Button(label, size_arg);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str());
+        };
+
+        auto handleScriptedAction = [](bool scriptArg)
+        {
+            return scriptArg;
+        };
+
+        return ActionHelper<bool>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::ListBox(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::ListBox(label, current_item, items_getter, data, items_count, height_in_items);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            const char* itemText = nullptr;
+            if (items_getter(data, *current_item, &itemText) && itemText)
+            {
+                Utils::ReportScriptableAction("SetImguiValue('%s', '%s')", pathToImGuiItem.c_str(), itemText);
+            }
+        };
+
+        auto handleScriptedAction = [&](AZStd::string scriptArg)
+        {
+            for (int i = 0; i < items_count; ++i)
+            {
+                const char* itemText = nullptr;
+                if (items_getter(data, i, &itemText) && itemText && scriptArg == itemText)
+                {
+                    *current_item = i;
+                    return true;
+                }
+            }
+
+            ReportScriptError(AZStd::string::format("List '%s' does not contain item '%s'", label, scriptArg.c_str()).c_str());
+            return false;
+        };
+
+        return ActionHelper<AZStd::string>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::Combo(label, current_item, items, items_count, height_in_items);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', '%s')", pathToImGuiItem.c_str(), items[*current_item]);
+        };
+
+        auto handleScriptedAction = [&](AZStd::string scriptArg)
+        {
+            for (int i = 0; i < items_count; ++i)
+            {
+                if (items[i] == scriptArg)
+                {
+                    *current_item = i;
+                    return true;
+                }
+            }
+
+            ReportScriptError(AZStd::string::format("Combo box '%s' does not contain item '%s'", label, scriptArg.c_str()).c_str());
+            return false;
+        };
+
+        return ActionHelper<AZStd::string>(label, imguiAction, reportScriptableAction, handleScriptedAction,
+            true /* It seems ImGui::Combo doesn't work with IsItemDeactivatedAfterChange() */);
+    }
+
+    bool ScriptableImGui::RadioButton(const char* label, int* v, int v_button)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::RadioButton(label, v, v_button);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str());
+        };
+
+        auto handleScriptedAction = [&](bool scriptArg)
+        {
+            if (scriptArg)
+            {
+                *v = v_button;
+                return true;
+            }
+            return false;
+        };
+
+        return ActionHelper<bool>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::SliderInt(label, v, v_min, v_max, format);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', %d)", pathToImGuiItem.c_str(), *v);
+        };
+
+        auto handleScriptedAction = [&](float scriptArg)
+        {
+            *v = aznumeric_cast<int>(scriptArg);
+            return true;
+        };
+
+        return ActionHelper<float>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::SliderFloat(label, v, v_min, v_max, format, power);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', %f)", pathToImGuiItem.c_str(), *v);
+        };
+
+        auto handleScriptedAction = [&](float scriptArg)
+        {
+            *v = scriptArg;
+            return true;
+        };
+
+        return ActionHelper<float>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::SliderFloat2(label, v, v_min, v_max, format, power);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', Vector2(%f, %f))", pathToImGuiItem.c_str(), v[0], v[1]);
+        };
+
+        auto handleScriptedAction = [&](AZ::Vector2 scriptArg)
+        {
+            v[0] = scriptArg.GetX();
+            v[1] = scriptArg.GetY();
+            return true;
+        };
+
+        return ActionHelper<AZ::Vector2>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    template <typename ImGuiActionType>
+    bool ScriptableImGui::ThreeComponentHelper(const char* label, float v[3], ImGuiActionType& imguiAction)
+    {
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', Vector3(%f, %f, %f))", pathToImGuiItem.c_str(), v[0], v[1], v[2]);
+        };
+
+        auto handleScriptedAction = [&](AZ::Vector3 scriptArg)
+        {
+            v[0] = scriptArg.GetX();
+            v[1] = scriptArg.GetY();
+            v[2] = scriptArg.GetZ();
+            return true;
+        };
+
+        return ActionHelper<AZ::Vector3>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::SliderFloat3(label, v, v_min, v_max, format, power);
+        };
+
+        return ThreeComponentHelper(label, v, imguiAction);
+    }
+
+    bool ScriptableImGui::ColorEdit3(const char* label, float v[3], ImGuiColorEditFlags flags)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::ColorEdit3(label, v, flags);
+        };
+
+        return ThreeComponentHelper(label, v, imguiAction);
+    }
+
+    bool ScriptableImGui::ColorPicker3(const char* label, float v[3], ImGuiColorEditFlags flags)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::ColorPicker3(label, v, flags);
+        };
+
+        return ThreeComponentHelper(label, v, imguiAction);
+    }
+
+    bool ScriptableImGui::SliderAngle(const char* label, float* v, float v_min, float v_max, const char* format)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::SliderAngle(label, v, v_min, v_max, format);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', %f)", pathToImGuiItem.c_str(), *v);
+        };
+
+        auto handleScriptedAction = [&](float scriptArg)
+        {
+            *v = scriptArg;
+            return true;
+        };
+
+        return ActionHelper<float>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::Selectable(label, selected, flags, size);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            // The "selected" value that's passed determines if the selectable is *currently* selected, and clicking the selectable toggles its state.
+            // So when someone clicks the selectable to change its state, we need to report the opposite of what the original state was.
+            Utils::ReportScriptableAction("SetImguiValue('%s', %s)", pathToImGuiItem.c_str(), (selected ? "false" : "true"));
+        };
+
+        auto handleScriptedAction = [&](bool scriptArg)
+        {
+            return scriptArg != selected;
+        };
+
+        return ActionHelper<bool>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::Selectable(label, p_selected, flags, size);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            // The "selected" value that's passed determines if the selectable is *currently* selected, and clicking the selectable toggles its state.
+            // So when someone clicks the selectable to change its state, we need to report the opposite of what the original state was.
+            Utils::ReportScriptableAction("SetImguiValue('%s', %s)", pathToImGuiItem.c_str(), (*p_selected ? "false" : "true"));
+        };
+
+        auto handleScriptedAction = [&](bool scriptArg)
+        {
+            if (scriptArg != *p_selected)
+            {
+                *p_selected = scriptArg;
+                return true;
+            }
+            return false;
+        };
+
+        return ActionHelper<bool>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+    bool ScriptableImGui::TreeNodeEx(const char* label, ImGuiSelectableFlags flags)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+        if (ImGui::TreeNodeEx(label, flags))
+        {
+            PushNameContext(label);
+            return true;
+        }
+
+        return false;
+    }
+
+    void ScriptableImGui::TreePop()
+    {
+        ImGui::TreePop();
+        PopNameContext();
+    }
+
+    bool ScriptableImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
+    {
+        AZ_DEBUG_STATIC_MEMEBER(instance, s_instance);
+
+        const AZStd::string pathToImGuiItem = MakeFullPath(label);
+
+        if (ImGui::BeginCombo(label, preview_value, flags))
+        {
+            PushNameContext(label);
+            return true;
+        }
+        else if (!s_instance->m_scriptedActions.empty())
+        {
+            // If a script is running, we return true so that imgui controls inside the combo box are checked.
+            // We also have to set a flag to prevent ScriptableImGui::EndCombo() from calling ImGui::EndCombo()
+            // because that is only allowed when ImGui::BeginCombo() returns true.
+            s_instance->m_isInScriptedComboPopup = true;
+            PushNameContext(label);
+            return true;
+        }
+
+        return false;
+    }
+
+    void ScriptableImGui::EndCombo()
+    {
+        if (s_instance->m_isInScriptedComboPopup)
+        {
+            s_instance->m_isInScriptedComboPopup = false;
+
+            // ScriptableImGui::BeginCombo() returned true even though ImGui::BeginCombo() didn't, so we aren't allowed
+            // to call ImGui::EndCombo() here.
+        }
+        else
+        {
+            ImGui::EndCombo();
+        }
+
+        PopNameContext();
+    }
+
+    bool ScriptableImGui::BeginMenu(const char* label, bool enabled)
+    {
+        // We don't use ActionHelper here because BeginMenu has to do things a bit different since there is a persistent popup.
+        // It has to run the script code before the ImGui code, and if there is a scripted action then force the menu to open.
+        // Also, we don't include a "scriptResult", just the "imResult", because we need to ensure that ImGui is in the actual
+        // state we are reporting back to the caller. Otherwise the internal state of ImGui could become invalid and crash.
+
+        const AZStd::string pathToImGuiItem = MakeFullPath(label);
+        
+        ActionItem actionItem = FindAndRemoveAction(pathToImGuiItem);
+        bool foundAction = !AZStd::holds_alternative<InvalidActionItem>(actionItem);
+        if (foundAction)
+        {
+            if (AZStd::holds_alternative<bool>(actionItem))
+            {
+                if (AZStd::get<bool>(actionItem))
+                {
+                    // Here we force the menu to open before arriving at ImGui::BeginMenu 
+                    ImGui::OpenPopup(label);
+                }
+            }
+            else
+            {
+                ReportScriptError(AZStd::string::format("Wrong data type used to set '%s'", label).c_str());
+            }
+        }
+
+        bool wasPopupOpen = ImGui::IsPopupOpen(label);
+
+        bool isPopupOpen = ImGui::BeginMenu(label, enabled);
+
+        if (isPopupOpen)
+        {
+            PushNameContext(label);
+
+            if (!wasPopupOpen && isPopupOpen)
+            {
+                Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str());
+            }
+        }
+
+        return isPopupOpen;
+    }
+
+    void ScriptableImGui::EndMenu()
+    {
+        ImGui::EndMenu();
+        PopNameContext();
+    }
+
+    bool ScriptableImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
+    {
+        auto imguiAction = [&]()
+        {
+            return ImGui::MenuItem(label, shortcut, selected, enabled);
+        };
+
+        auto reportScriptableAction = [&](const AZStd::string& pathToImGuiItem)
+        {
+            Utils::ReportScriptableAction("SetImguiValue('%s', true)", pathToImGuiItem.c_str());
+        };
+
+        auto handleScriptedAction = [](bool scriptArg)
+        {
+            return scriptArg;
+        };
+
+        return ActionHelper<bool>(label, imguiAction, reportScriptableAction, handleScriptedAction);
+    }
+
+
+} // namespace AtomSampleViewer

+ 162 - 0
Gem/Code/Source/Automation/ScriptableImGui.h

@@ -0,0 +1,162 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/RTTI/RTTI.h>
+#include <AzCore/std/string/string.h>
+#include <AzCore/std/containers/unordered_map.h>
+#include <AzCore/Math/Vector3.h>
+#include <imgui/imgui.h>
+
+#define SCRIPTABLE_IMGUI
+
+#ifdef SCRIPTABLE_IMGUI
+#define Scriptable_ImGui AtomSampleViewer::ScriptableImGui
+#endif
+
+namespace AtomSampleViewer
+{
+    class ScriptManager;
+
+    //! Wraps the ImGui API in a reflection system that automatically exposes ImGui data elements
+    //! to the scripting system. It enhances standard ImGui functions to check for scripted
+    //! actions that can perform the same actions as a user.
+    class ScriptableImGui final
+    {
+        friend class ScriptManager;
+    public:
+        AZ_CLASS_ALLOCATOR(ScriptableImGui, AZ::SystemAllocator, 0);
+
+        //! Utility for calling ScriptableImGui::PushNameContext and ScriptableImGui::PopNameContext
+        class ScopedNameContext final
+        {
+        public:
+            ScopedNameContext(const AZStd::string& nameContext) { ScriptableImGui::PushNameContext(nameContext); }
+            ~ScopedNameContext() { ScriptableImGui::PopNameContext(); }
+        };
+
+        //! This can be used to add some context around the ImGui labels that are exposed to the script system.
+        //! Each call to PushNameContext() will add a prefix the ImGui labels to form the script field IDs.
+        //! For example, the following will result in a script field ID of "A/B/MyButton" instead of just "MyButton".
+        //!     PushNameContext("A");
+        //!     PushNameContext("B");
+        //!     Button("MyButton",...);
+        //!     PopNameContext();
+        //!     PopNameContext();
+        //! This is especially useful for disambiguating similar ImGui labels.
+        //! There is also a ScopedNameContext utility class for managing the push/pop using the call stack.
+        static void PushNameContext(const AZStd::string& nameContext);
+        static void PopNameContext();
+
+        //////////////////////////////////////////////////////////////////
+        // ImGui bridge functions...
+        // These follow the same API as the corresponding ImGui functions.
+        // Add more bridge functions as needed.
+
+        static bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0);
+        static void End();
+
+        static bool Checkbox(const char* label, bool* v);
+        static bool Button(const char* label, const ImVec2& size_arg = ImVec2(0, 0));
+        static bool ListBox(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items = -1);
+        static bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1);
+
+        static bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format = "%d");
+        static bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f);
+        static bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", float power = 1.0f);
+        static bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", float power = 1.0f);
+        static bool SliderAngle(const char* label, float* v, float v_min = -360.0f, float v_max = 360.0f, const char* format = "%.0f deg");
+
+        static bool ColorEdit3(const char* label, float v[3], ImGuiColorEditFlags flags);
+        static bool ColorPicker3(const char* label, float v[3], ImGuiColorEditFlags flags);
+
+        static bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0));
+        static bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0));
+
+        static bool TreeNodeEx(const char* label, ImGuiSelectableFlags flags = 0);
+        static void TreePop();
+
+        static bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0);
+        static void EndCombo();
+
+        static bool BeginMenu(const char* label, bool enabled = true);
+        static void EndMenu();
+        static bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true);
+
+        static bool RadioButton(const char* label, int* v, int v_button);
+
+    private:
+
+        /////////////////////////////////////////////////////////////////////////////////////////////////
+        // Private API for ScriptManager to call...
+
+        static void Create();
+        static void Destory();
+
+        //! Call this every frame to report errors when scripted actions aren't consumed through ImGui API function calls.
+        //! This usually indicates that a script is trying to manipulate ImGui elements that don't exist.
+        static void CheckAllActionsConsumed();
+
+        //! Clears any scripted actions that were scheduled. This should be called every frame to make sure old actions
+        //! don't hang around indefinitely and get consumed later and cause unexpected behavior.
+        static void ClearActions();
+
+        //! These functions are called through scripts to schedule a scripted action.
+        //! This data will be consumed by subsequent calls to ImGui API functions.
+        //! @param pathToImGuiItem - full path to an ImGui item, including the name context
+        static void SetBool(const AZStd::string& name, bool value);
+        static void SetNumber(const AZStd::string& name, float value);
+        static void SetVector(const AZStd::string& name, const AZ::Vector2& value);
+        static void SetVector(const AZStd::string& name, const AZ::Vector3& value);
+        static void SetString(const AZStd::string& name, const AZStd::string& value);
+
+        /////////////////////////////////////////////////////////////////////////////////////////////////
+
+        using InvalidActionItem = AZStd::monostate;
+        // Note we don't include an int type because Lua only supports floats
+        using ActionItem = AZStd::variant<InvalidActionItem, bool, float, AZ::Vector2, AZ::Vector3, AZStd::string>;
+
+        //! This utility function factors out common steps that most of the ImGui API bridge functions must perform.
+        template<typename ActionDataT>
+        static bool ActionHelper(
+            const char* label,
+            AZStd::function<bool()> imguiAction,
+            AZStd::function<void(const AZStd::string& /*pathToImGuiItem*/)> reportScriptableAction,
+            AZStd::function<bool(ActionDataT /*scriptArg*/)> handleScriptedAction,
+            bool shouldReportScriptableActionAfterAnyChange = false);
+
+        template<typename ImGuiActionType>
+        static bool ThreeComponentHelper(const char* label, float v[3], ImGuiActionType& imguiAction);
+
+        //! Finds a scheduled script action and removes it from the list of actions.
+        //! @param pathToImGuiItem - full path to an ImGui item, including the name context
+        static ActionItem FindAndRemoveAction(const AZStd::string& pathToImGuiItem);
+
+        //! Makes a full script field ID path for the given ImGui label, by prefixing the current name context
+        static AZStd::string MakeFullPath(const AZStd::string& forLabel);
+
+        //! Utility function to ensure all script errors use a similar format
+        static void ReportScriptError(const char* message);
+
+        //! Provides a name context prefix to script field IDs for disambiguation.
+        AZStd::vector<AZStd::string> m_nameContextStack; 
+
+        using ActionMap = AZStd::unordered_map<AZStd::string, ActionItem>;
+        ActionMap m_scriptedActions;
+
+        bool m_isInScriptedComboPopup = false;
+
+        static ScriptableImGui* s_instance;
+    };
+
+} // namespace AtomSampleViewer

+ 190 - 0
Gem/Code/Source/AuxGeomExampleComponent.cpp

@@ -0,0 +1,190 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <AuxGeomExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/RPI.Public/AuxGeom/AuxGeomFeatureProcessorInterface.h>
+#include <Atom/RPI.Public/AuxGeom/AuxGeomDraw.h>
+
+#include <AzCore/Component/Entity.h>
+#include <AzCore/Math/Matrix3x3.h>
+
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Components/CameraBus.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+
+#include <AuxGeomSharedDrawFunctions.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    void AuxGeomExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<AuxGeomExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    AuxGeomExampleComponent::AuxGeomExampleComponent()
+    {
+    }
+
+    void AuxGeomExampleComponent::LoadConfigFiles()
+    {
+    }
+
+    void AuxGeomExampleComponent::Activate()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+
+        Camera::CameraRequestBus::Event(GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFarClipDistance,
+            200.f);
+
+        m_imguiSidebar.Activate();
+
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void AuxGeomExampleComponent::Deactivate()
+    {
+        AZ::TickBus::Handler::BusDisconnect();
+
+        m_imguiSidebar.Deactivate();
+
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+    }
+    
+    void AuxGeomExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
+    {
+        if (m_imguiSidebar.Begin())
+        {
+            ImGui::Text("Draw Options");
+
+            if (ScriptableImGui::Button("Select All"))
+            {
+                m_drawBackgroundBox = true;
+                m_drawThreeGridsOfPoints = true;
+                m_drawAxisLines = true;
+                m_drawLines = true;
+                m_drawTriangles = true;
+                m_drawShapes = true;
+                m_drawBoxes = true;
+                m_drawManyPrimitives = true;
+                m_drawDepthTestPrimitives = true;
+                m_draw2DWireRect = true;
+            }
+
+            if (ScriptableImGui::Button("Unselect All"))
+            {
+                m_drawBackgroundBox = false;
+                m_drawThreeGridsOfPoints = false;
+                m_drawAxisLines = false;
+                m_drawLines = false;
+                m_drawTriangles = false;
+                m_drawShapes = false;
+                m_drawBoxes = false;
+                m_drawManyPrimitives = false;
+                m_drawDepthTestPrimitives = false;
+                m_draw2DWireRect = false;
+            }
+            ImGui::Indent();
+
+            ScriptableImGui::Checkbox("Draw background box", &m_drawBackgroundBox);
+            ScriptableImGui::Checkbox("Draw three grids of points", &m_drawThreeGridsOfPoints);
+            ScriptableImGui::Checkbox("Draw axis lines", &m_drawAxisLines);
+            ScriptableImGui::Checkbox("Draw lines", &m_drawLines);
+            ScriptableImGui::Checkbox("Draw triangles", &m_drawTriangles);
+            ScriptableImGui::Checkbox("Draw shapex", &m_drawShapes);
+            ScriptableImGui::Checkbox("Draw boxes", &m_drawBoxes);
+            ScriptableImGui::Checkbox("Draw many primitives", &m_drawManyPrimitives);
+            ScriptableImGui::Checkbox("Draw depth test primitives", &m_drawDepthTestPrimitives);
+            ScriptableImGui::Checkbox("Draw 2d wire rect", &m_draw2DWireRect);
+
+            ImGui::Unindent();
+
+            m_imguiSidebar.End();
+        }
+
+
+        // Currently this does this one thing, the intention is that this sample has a sidebar to allow selection of different
+        // examples/tests
+        DrawSampleOfAllAuxGeom();
+    }
+
+    void AuxGeomExampleComponent::DrawSampleOfAllAuxGeom() const
+    {
+        auto defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        if (auto auxGeom = AZ::RPI::AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(defaultScene))
+        {
+            if (m_drawBackgroundBox)
+            {
+                DrawBackgroundBox(auxGeom);
+            }
+
+            if (m_drawThreeGridsOfPoints)
+            {
+                DrawThreeGridsOfPoints(auxGeom);
+            }
+
+            if (m_drawAxisLines)
+            {
+                DrawAxisLines(auxGeom);
+            }
+
+            if (m_drawLines)
+            {
+                DrawLines(auxGeom);
+            }
+
+            if (m_drawTriangles)
+            {
+                DrawTriangles(auxGeom);
+            }
+
+            if (m_drawShapes)
+            {
+                DrawShapes(auxGeom);
+            }
+
+            if (m_drawBoxes)
+            {
+                DrawBoxes(auxGeom);
+            }
+
+            if (m_drawManyPrimitives)
+            {
+                DrawManyPrimitives(auxGeom);
+            }
+
+            if (m_drawDepthTestPrimitives)
+            {
+                DrawDepthTestPrimitives(auxGeom);
+            }
+
+            if (m_draw2DWireRect)
+            {
+                Draw2DWireRect(auxGeom, AZ::Colors::Red, 1.0f);
+            }
+        }
+    }
+
+} // namespace AtomSampleViewer

+ 67 - 0
Gem/Code/Source/AuxGeomExampleComponent.h

@@ -0,0 +1,67 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <Utils/Utils.h>
+#include <Utils/ImGuiSidebar.h>
+
+namespace AtomSampleViewer
+{
+    class AuxGeomExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(AuxGeomExampleComponent, "{C7AC0D17-84D9-42A2-9EBA-2358C0F13074}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        AuxGeomExampleComponent();
+        ~AuxGeomExampleComponent() override = default;
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+    private: // functions
+
+        void LoadConfigFiles();
+
+        // Functions for each display option (currently there is only one
+        void DrawSampleOfAllAuxGeom() const;
+
+        // Functions used by DrawSampleOfAllAuxGeom
+
+    private: // data
+                
+        ImGuiSidebar m_imguiSidebar;
+
+        // draw options
+        bool m_drawBackgroundBox = true;
+        bool m_drawThreeGridsOfPoints = true;
+        bool m_drawAxisLines = true;
+        bool m_drawLines = true;
+        bool m_drawTriangles = true;
+        bool m_drawShapes = true;
+        bool m_drawBoxes = true;
+        bool m_drawManyPrimitives = true;
+        bool m_drawDepthTestPrimitives = true;
+        bool m_draw2DWireRect = true;
+    };
+} // namespace AtomSampleViewer

+ 1052 - 0
Gem/Code/Source/AuxGeomSharedDrawFunctions.cpp

@@ -0,0 +1,1052 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include "AuxGeomSharedDrawFunctions.h"
+
+#include <AzCore/base.h>
+#include <AzCore/Math/Color.h>
+#include <AzCore/Math/Vector3.h>
+#include <AzCore/Math/Aabb.h>
+#include <AzCore/Math/Obb.h>
+
+#include <AzCore/Casting/numeric_cast.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+    using namespace RPI;
+    using namespace Colors;
+
+    // Create some semi-transparent colors
+    const AZ::Color BlackAlpha     (0.0f, 0.0f, 0.0f, 0.5f);
+    const AZ::Color WhiteAlpha     (1.0f, 1.0f, 1.0f, 0.5f);
+
+    const AZ::Color RedAlpha       (1.0f, 0.0f, 0.0f, 0.5f);
+    const AZ::Color GreenAlpha     (0.0f, 1.0f, 0.0f, 0.5f);
+    const AZ::Color BlueAlpha      (0.0f, 0.0f, 1.0f, 0.5f);
+
+    const AZ::Color YellowAlpha    (0.5f, 0.5f, 0.0f, 0.5f);
+    const AZ::Color CyanAlpha      (0.0f, 0.5f, 0.5f, 0.5f);
+    const AZ::Color MagentaAlpha   (0.5f, 0.0f, 0.5f, 0.5f);
+
+    const AZ::Color LightGray      (0.8f, 0.8f, 0.8, 1.0f);
+    const AZ::Color DarkGray       (0.2f, 0.2f, 0.2, 1.0f);
+
+    void DrawBackgroundBox(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        // Draw a big cube using DrawTriangles to create a background for the other tests.
+        // Use triangles rather than an AABB because triangles have back-face culling disabled.
+        // All other test geometries are drawn inside this big cube.
+        float cubeHalfWidth = 80.0f;
+        float left = -cubeHalfWidth;
+        float right = cubeHalfWidth;
+        float top = cubeHalfWidth;
+        float bottom = -cubeHalfWidth;
+        float front = cubeHalfWidth;
+        float back = -cubeHalfWidth;
+        const uint32_t NumCubePoints = 8;
+        AZ::Vector3 cubePoints[NumCubePoints] =
+        {
+            AZ::Vector3(left, front, top), AZ::Vector3(right, front, top), AZ::Vector3(right, front, bottom), AZ::Vector3(left, front, bottom),
+            AZ::Vector3(left, back, top), AZ::Vector3(right, back, top), AZ::Vector3(right, back, bottom), AZ::Vector3(left, back, bottom),
+        };
+        const uint32_t NumCubeIndicies = 36;
+        uint32_t cubeIndicies[NumCubeIndicies] =
+        {
+            0, 1, 2,  2, 3, 0,  // front face
+            4, 5, 6,  6, 7, 4,  // back face (no back-face culling)
+            0, 3, 7,  7, 4, 0,  // left
+            1, 2, 6,  6, 5, 1,  // right
+            0, 1, 5,  5, 4, 0,  // top
+            2, 3, 7,  7, 6, 2,  // bottom
+        };
+
+        // Make the cube dark gray on the top face, blending to light gray on the bottom face
+        AZ::Color cubeColors[NumCubePoints] = { DarkGray, DarkGray, LightGray, LightGray, DarkGray, DarkGray, LightGray, LightGray };
+
+        // Draw as opaque cube with multiple colors and shared vertices
+        AuxGeomDraw::AuxGeomDynamicIndexedDrawArguments drawArgs;
+        drawArgs.m_verts = cubePoints;
+        drawArgs.m_vertCount = NumCubePoints;
+        drawArgs.m_indices = cubeIndicies;
+        drawArgs.m_indexCount = NumCubeIndicies;
+        drawArgs.m_colors = cubeColors;
+        drawArgs.m_colorCount = NumCubePoints;
+        drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Opaque;
+        auxGeom->DrawTriangles(drawArgs);
+    }
+
+    void DrawThreeGridsOfPoints(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        AZ::u8 pointSize = 10;   // DX12 API ignores point size, works on Vulkan
+
+        // draw three grids of points
+
+        const uint32_t NumPlanePointsPerAxis = 16; // must be even
+        const uint32_t NumPlanePoints = NumPlanePointsPerAxis * NumPlanePointsPerAxis;
+        float gridHalfWidth = 0.1f;
+        float gridSpacing = gridHalfWidth/aznumeric_cast<float>(NumPlanePointsPerAxis/2);
+        AZ::Vector3 origin(0.0f, 0.0f, 2.0f);
+
+        ///////////////////////////////////////////////////////////////////////
+        //  1st grid of points is in plane of x = 0, draw in red
+        float x, y, z;
+        y = -gridHalfWidth;
+        for (int yIndex = 0; yIndex < NumPlanePointsPerAxis; ++yIndex, y += gridSpacing)
+        {
+            z = -gridHalfWidth;
+            for (int zIndex = 0; zIndex <= NumPlanePointsPerAxis; ++zIndex, z += gridSpacing)
+            {
+                AZ::Vector3 vert = origin + AZ::Vector3(0.0f, y, z);
+                AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+                drawArgs.m_verts = &vert;
+                drawArgs.m_vertCount = 1;
+                drawArgs.m_colors = &Red;
+                drawArgs.m_colorCount = 1;
+                drawArgs.m_size = pointSize;
+                auxGeom->DrawPoints(drawArgs);
+            }
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // 2nd grid of points is in plane of y = 0, draw in green with one draw call
+        AZ::Vector3 planePoints[NumPlanePointsPerAxis * NumPlanePointsPerAxis];
+        uint32_t pointIndex = 0;
+        x = -gridHalfWidth;
+        for (int xIndex = 0; xIndex < NumPlanePointsPerAxis; ++xIndex, x += gridSpacing)
+        {
+           z = -gridHalfWidth;
+           for (int zIndex = 0; zIndex < NumPlanePointsPerAxis; ++zIndex, z += gridSpacing)
+           {
+               planePoints[pointIndex++] = origin + AZ::Vector3(x, 0.0f, z);
+           }
+        }
+        AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+        drawArgs.m_verts = planePoints;
+        drawArgs.m_vertCount = NumPlanePoints;
+        drawArgs.m_colors = &Green;
+        drawArgs.m_colorCount = 1;
+        drawArgs.m_size = pointSize;
+        auxGeom->DrawPoints(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        // 3rd grid of points is in plane of z = 0, draw in multiple colors with one draw call
+        AZ::Color pointColors[NumPlanePointsPerAxis * NumPlanePointsPerAxis];
+        pointIndex = 0;
+        float opacity = 0.0f;
+        x = -gridHalfWidth;
+        for (int xIndex = 0; xIndex < NumPlanePointsPerAxis; ++xIndex, x += gridSpacing)
+        {
+            y = -gridHalfWidth;
+            for (int yIndex = 0; yIndex < NumPlanePointsPerAxis; ++yIndex, y += gridSpacing)
+            {
+                planePoints[pointIndex] = origin + AZ::Vector3(x, y, 0.0f);
+                pointColors[pointIndex] = AZ::Color(0.0f, 0.0f, 1.0f, opacity);
+                ++pointIndex;
+                opacity += 1.0f / NumPlanePoints;
+            }
+        }
+        drawArgs.m_verts = planePoints;
+        drawArgs.m_vertCount = NumPlanePoints;
+        drawArgs.m_colors = pointColors;
+        drawArgs.m_colorCount = NumPlanePoints;
+        drawArgs.m_size = pointSize;
+        drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Translucent;
+        auxGeom->DrawPoints(drawArgs);
+    }
+
+    void DrawAxisLines(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        // draw a line for each axis with triangles indicating direction and spheres on the ends
+
+        AZ::u8 lineWidth = 1;   // currently we don't support width on lines
+
+        ///////////////////////////////////////////////////////////////////////
+        // Draw three lines for the axes
+        float axisLength = 30.0f;
+        AZ::Vector3 verts[3] = {AZ::Vector3( -axisLength, 0, 0 ), AZ::Vector3( axisLength, 0, 0 )};
+        AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+        drawArgs.m_verts = verts;
+        drawArgs.m_vertCount = 2;
+        drawArgs.m_colors = &Red;
+        drawArgs.m_colorCount = 1;
+        drawArgs.m_size = lineWidth;
+        auxGeom->DrawLines(drawArgs);
+
+        verts[0] = AZ::Vector3( 0, -axisLength, 0 ); verts[1] = AZ::Vector3( 0, axisLength, 0 );
+        drawArgs.m_colors = &Green;
+        auxGeom->DrawLines(drawArgs);
+
+        verts[0] = AZ::Vector3( 0, 0, -axisLength ); verts[1] = AZ::Vector3( 0, 0, axisLength );
+        drawArgs.m_colors = &Blue;
+        auxGeom->DrawLines(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        // Next, draw a couple of triangles on each axis to indicate increasing direction
+        float triLength = 1.0f;
+        float triHalfWidth = 0.3f;
+        float start = 2.5f;
+        verts[0] = AZ::Vector3(start + triLength, 0, 0); verts[1] = AZ::Vector3(start, -triHalfWidth, 0); verts[2] = AZ::Vector3(start, triHalfWidth, 0);
+        drawArgs.m_colors = &Red;
+        drawArgs.m_vertCount = 3;
+        auxGeom->DrawTriangles(drawArgs);
+
+        verts[1] = AZ::Vector3(start, 0, triHalfWidth); verts[2] = AZ::Vector3(start, 0, -triHalfWidth);
+        auxGeom->DrawTriangles(drawArgs);
+
+        verts[0] = AZ::Vector3(0, start + triLength, 0); verts[1] = AZ::Vector3(0, start, -triHalfWidth); verts[2] = AZ::Vector3(0, start, triHalfWidth);
+        drawArgs.m_colors = &Green;
+        auxGeom->DrawTriangles(drawArgs);
+
+        verts[1] = AZ::Vector3(triHalfWidth, start, 0); verts[2] = AZ::Vector3(-triHalfWidth, start, 0);
+        auxGeom->DrawTriangles(drawArgs);
+
+        verts[0] = AZ::Vector3(0, 0, start + triLength); verts[1] = AZ::Vector3(-triHalfWidth, 0, start); verts[2] = AZ::Vector3(triHalfWidth, 0, start);
+        drawArgs.m_colors = &Blue;
+        auxGeom->DrawTriangles(drawArgs);
+
+        verts[1] = AZ::Vector3(0, triHalfWidth, start); verts[2] = AZ::Vector3(0, -triHalfWidth, start);
+        auxGeom->DrawTriangles(drawArgs);
+    }
+
+    void DrawLines(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        float halfLength = 0.25f;
+        AZ::Vector3 xVec(halfLength, 0.0f, 0.0f);
+        AZ::Vector3 yVec(0.0f, halfLength, 0.0f);
+        AZ::Vector3 zVec(0.0f, 0.0f, halfLength);
+
+        ///////////////////////////////////////////////////////////////////////
+        // Draw a 3d cross all one color using DrawLines (opaque)
+        {
+            AZ::Vector3 center(-1.0f, 1.0f, 0.0f);
+            AZ::Vector3 points[] = { center - xVec, center + xVec, center - zVec, center + zVec, center - yVec, center + yVec };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 6;
+            drawArgs.m_colors = &Black;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // Draw a 3d cross all one color using DrawLines (translucent)
+        {
+            AZ::Vector3 center(-2.0f, 1.0f, 0.0f);
+            AZ::Vector3 points[] = { center - xVec, center + xVec, center - zVec, center + zVec, center - yVec, center + yVec };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 6;
+            drawArgs.m_colors = &BlackAlpha;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // Draw a 3d cross in three colors using DrawLines (opaque)
+        {
+            AZ::Vector3 center(-1.0f, 2.0f, 0.0f);
+            AZ::Vector3 points[] = { center - xVec, center + xVec, center - zVec, center + zVec, center - yVec, center + yVec };
+            AZ::Color colors[] = { Red, Yellow, Green, Cyan, Blue, Magenta };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 6;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 6;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // Draw a 3d cross in three colors using DrawLines (translucent)
+        {
+            AZ::Vector3 center(-2.0f, 2.0f, 0.0f);
+            AZ::Vector3 points[] = { center - xVec, center + xVec, center - zVec, center + zVec, center - yVec, center + yVec };
+            AZ::Color colors[] = { RedAlpha, Yellow, GreenAlpha, Cyan, BlueAlpha, Magenta };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 6;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 6;
+            drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Translucent;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a wireframe pyramid using 5 points and 16 indices in one color (opaque)
+        {
+            AZ::Vector3 baseCenter(-1.0f, 3.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec, baseCenter + zVec };
+            uint32_t indices[] = { 0, 1, 1, 2, 2, 3, 3, 0,   0, 4, 1, 4, 2, 4, 3, 4 };
+            AuxGeomDraw::AuxGeomDynamicIndexedDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 5;
+            drawArgs.m_indices = indices;
+            drawArgs.m_indexCount = 16;
+            drawArgs.m_colors = &Black;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a wireframe pyramid using 5 points and 16 indices in one color (translucent)
+        {
+            AZ::Vector3 baseCenter(-2.0f, 3.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec, baseCenter + zVec };
+            uint32_t indices[] = { 0, 1, 1, 2, 2, 3, 3, 0,   0, 4, 1, 4, 2, 4, 3, 4 };
+            AuxGeomDraw::AuxGeomDynamicIndexedDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 5;
+            drawArgs.m_indices = indices;
+            drawArgs.m_indexCount = 16;
+            drawArgs.m_colors = &BlackAlpha;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a wireframe pyramid using 5 points and 16 indices in many colors (opaque)
+        {
+            AZ::Vector3 baseCenter(-1.0f, 4.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec, baseCenter + zVec };
+            uint32_t indices[] = { 0, 1, 1, 2, 2, 3, 3, 0,   0, 4, 1, 4, 2, 4, 3, 4 };
+            AZ::Color colors[] = { Red, Green, Blue, Yellow, Black };
+            AuxGeomDraw::AuxGeomDynamicIndexedDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 5;
+            drawArgs.m_indices = indices;
+            drawArgs.m_indexCount = 16;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 5;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a wireframe pyramid using 5 points and 16 indices in many colors (translucent)
+        {
+            AZ::Vector3 baseCenter(-2.0f, 4.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec, baseCenter + zVec };
+            uint32_t indices[] = { 0, 1, 1, 2, 2, 3, 3, 0,   0, 4, 1, 4, 2, 4, 3, 4 };
+            AZ::Color colors[] = { RedAlpha, GreenAlpha, BlueAlpha, YellowAlpha, Black };
+            AuxGeomDraw::AuxGeomDynamicIndexedDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 5;
+            drawArgs.m_indices = indices;
+            drawArgs.m_indexCount = 16;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 5;
+            drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Translucent;
+            auxGeom->DrawLines(drawArgs);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a closed square using a polyline using 4 points in one color (opaque)
+        {
+            AZ::Vector3 baseCenter(-1.0f, 5.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = &Black;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Closed);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a closed square using a polyline using 4 points in one color (translucent)
+        {
+            AZ::Vector3 baseCenter(-2.0f, 5.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = &BlackAlpha;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Closed);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw an open square using a polyline using 4 points in one color (opaque)
+        {
+            AZ::Vector3 baseCenter(-1.0f, 6.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = &Black;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Open);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw an open square using a polyline using 4 points in one color (translucent)
+        {
+            AZ::Vector3 baseCenter(-2.0f, 6.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = &BlackAlpha;
+            drawArgs.m_colorCount = 1;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Open);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a closed square using a polyline using 4 points in many colors (opaque)
+        {
+            AZ::Vector3 baseCenter(-1.0f, 7.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AZ::Color colors[] = { Red, Green, Blue, Yellow };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 4;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Closed);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw a closed square using a polyline using 4 points in many colors (translucent)
+        {
+            AZ::Vector3 baseCenter(-2.0f, 7.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AZ::Color colors[] = { RedAlpha, GreenAlpha, BlueAlpha, YellowAlpha };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 4;
+            drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Translucent;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Closed);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw an open square using a polyline using 4 points in many colors (opaque)
+        {
+            AZ::Vector3 baseCenter(-1.0f, 8.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - yVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AZ::Color colors[] = { Red, Green, Blue, Yellow };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 4;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Open);
+        }
+
+        ///////////////////////////////////////////////////////////////////////
+        // draw an open square using a polyline using 4 points in many colors (translucent)
+        {
+            AZ::Vector3 baseCenter(-2.0f, 8.0f, 0.0f);
+            AZ::Vector3 points[] = { baseCenter - xVec - zVec, baseCenter + xVec - yVec, baseCenter + xVec + yVec, baseCenter - xVec + yVec };
+            AZ::Color colors[] = { RedAlpha, GreenAlpha, BlueAlpha, YellowAlpha };
+            AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+            drawArgs.m_verts = points;
+            drawArgs.m_vertCount = 4;
+            drawArgs.m_colors = colors;
+            drawArgs.m_colorCount = 4;
+            drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Translucent;
+            auxGeom->DrawPolylines(drawArgs, AuxGeomDraw::PolylineEnd::Open);
+        }
+    }
+
+    void DrawTriangles(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        // Draw a mixture of opaque and translucent triangles to test distance sorting of primitives
+        float width = 2.0f;
+        float left = -5.0f;
+        float right = left + width;
+        float bottom = 2.0f;
+        float top = bottom + width;
+        float spacing = 1.0f;
+        float yStart = -10.0f;
+        float y = yStart;
+        ///////////////////////////////////////////////////////////////////////
+        AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+        AZ::Vector3 verts[3] = {AZ::Vector3(left, y, top), AZ::Vector3(right, y, top), AZ::Vector3(right, y, bottom)};
+        drawArgs.m_verts = verts;
+        drawArgs.m_vertCount = 3;
+        drawArgs.m_colors = &Black;
+        drawArgs.m_colorCount = 1;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        y += spacing;
+        verts[0] = AZ::Vector3(right, y, top); verts[1] = AZ::Vector3(right, y, bottom); verts[2] = AZ::Vector3(left, y, bottom);
+        drawArgs.m_colors = &BlackAlpha;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        y += spacing;
+        verts[0] = AZ::Vector3(left, y, top); verts[1] = AZ::Vector3(right, y, top); verts[2] = AZ::Vector3(right, y, bottom);
+        drawArgs.m_colors = &Red;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        y += spacing;
+        verts[0] = AZ::Vector3(right, y, top); verts[1] = AZ::Vector3(right, y, bottom); verts[2] = AZ::Vector3(left, y, bottom);
+        drawArgs.m_colors = &RedAlpha;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        y += spacing;
+        verts[0] = AZ::Vector3(left, y, top); verts[1] = AZ::Vector3(right, y, top); verts[2] = AZ::Vector3(right, y, bottom);
+        drawArgs.m_colors = &Green;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        y += spacing;
+        verts[0] = AZ::Vector3(right, y, top); verts[1] = AZ::Vector3(right, y, bottom); verts[2] = AZ::Vector3(left, y, bottom);
+        drawArgs.m_colors = &GreenAlpha;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        y += spacing;
+        verts[0] = AZ::Vector3(left, y, top); verts[1] = AZ::Vector3(right, y, top); verts[2] = AZ::Vector3(right, y, bottom);
+        drawArgs.m_colors = &Blue;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        y += spacing;
+        verts[0] = AZ::Vector3(right, y, top); verts[1] = AZ::Vector3(right, y, bottom); verts[2] = AZ::Vector3(left, y, bottom);
+        drawArgs.m_colors = &BlueAlpha;
+        auxGeom->DrawTriangles(drawArgs);
+
+
+        ///////////////////////////////////////////////////////////////////////
+        // do the same thing but in 2 AuxGeom draw calls - one for the opaque and one for the translucent
+        // Note that this will mean that the 5 translucent depth sort together and not separately
+        left = -8.0f;
+        right = left + width;
+        const uint32_t NumPoints = 12;
+        AZ::Vector3 opaquePoints[NumPoints] =
+        {
+            AZ::Vector3(left, yStart + spacing * 0, top), AZ::Vector3(right, yStart + spacing * 0, top), AZ::Vector3(right, yStart + spacing * 0, bottom),
+            AZ::Vector3(left, yStart + spacing * 2, top), AZ::Vector3(right, yStart + spacing * 2, top), AZ::Vector3(right, yStart + spacing * 2, bottom),
+            AZ::Vector3(left, yStart + spacing * 4, top), AZ::Vector3(right, yStart + spacing * 4, top), AZ::Vector3(right, yStart + spacing * 4, bottom),
+            AZ::Vector3(left, yStart + spacing * 6, top), AZ::Vector3(right, yStart + spacing * 6, top), AZ::Vector3(right, yStart + spacing * 6, bottom),
+        };
+        AZ::Vector3 transPoints[NumPoints] =
+        {
+            AZ::Vector3(right, yStart + spacing * 1, top), AZ::Vector3(right, yStart + spacing * 1, bottom), AZ::Vector3(left, yStart + spacing * 1, bottom),
+            AZ::Vector3(right, yStart + spacing * 3, top), AZ::Vector3(right, yStart + spacing * 3, bottom), AZ::Vector3(left, yStart + spacing * 3, bottom),
+            AZ::Vector3(right, yStart + spacing * 5, top), AZ::Vector3(right, yStart + spacing * 5, bottom), AZ::Vector3(left, yStart + spacing * 5, bottom),
+            AZ::Vector3(right, yStart + spacing * 7, top), AZ::Vector3(right, yStart + spacing * 7, bottom), AZ::Vector3(left, yStart + spacing * 7, bottom),
+        };
+        AZ::Color opaqueColors[NumPoints] = {
+            Black, Black, Black,
+            Red, Red, Red,
+            Green, Green, Green,
+            Blue, Blue, Blue,
+        };
+        AZ::Color transColors[NumPoints] = {
+            BlackAlpha, BlackAlpha, BlackAlpha,
+            RedAlpha, RedAlpha, RedAlpha,
+            GreenAlpha, GreenAlpha, GreenAlpha,
+            BlueAlpha, BlueAlpha, BlueAlpha,
+        };
+        ///////////////////////////////////////////////////////////////////////
+        // opaque triangles
+        drawArgs.m_verts = opaquePoints;
+        drawArgs.m_vertCount = NumPoints;
+        drawArgs.m_colors = opaqueColors;
+        drawArgs.m_colorCount = NumPoints;
+        drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Opaque;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        // translucent triangles
+        drawArgs.m_verts = transPoints;
+        drawArgs.m_colors = transColors;
+        drawArgs.m_opacityType = AuxGeomDraw::OpacityType::Translucent;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        // Draw cubes using indexed draws to test shared vertices
+        left = -12.0f;
+        right = left + width;
+        float front = yStart;
+        float back = front + width;
+        const uint32_t NumCubePoints = 8;
+        AZ::Vector3 cubePoints[NumCubePoints] =
+        {
+            AZ::Vector3(left, front, top), AZ::Vector3(right, front, top), AZ::Vector3(right, front, bottom), AZ::Vector3(left, front, bottom),
+            AZ::Vector3(left, back, top), AZ::Vector3(right, back, top), AZ::Vector3(right, back, bottom), AZ::Vector3(left, back, bottom),
+        };
+        const uint32_t NumCubeIndicies = 36;
+        uint32_t cubeIndicies[NumCubeIndicies] =
+        {
+            0, 1, 2,  2, 3, 0,  // front face
+            4, 5, 6,  6, 7, 4,  // back face (no back-face culling)
+            0, 3, 7,  7, 4, 0,  // left
+            1, 2, 6,  6, 5, 1,  // right
+            0, 1, 5,  5, 4, 0,  // top
+            2, 3, 7,  7, 6, 2,  // bottom
+        };
+        AZ::Color cubeColors[NumCubePoints] = { Red, Green, Blue, Black, RedAlpha, GreenAlpha, BlueAlpha, BlackAlpha };
+
+        ///////////////////////////////////////////////////////////////////////
+        // Opaque cube all one color
+        AuxGeomDraw::AuxGeomDynamicIndexedDrawArguments indexedDrawArgs;
+        indexedDrawArgs.m_verts = cubePoints;
+        indexedDrawArgs.m_vertCount = NumCubePoints;
+        indexedDrawArgs.m_indices = cubeIndicies;
+        indexedDrawArgs.m_indexCount = NumCubeIndicies;
+        indexedDrawArgs.m_colors = &Red;
+        indexedDrawArgs.m_colorCount = 1;
+        auxGeom->DrawTriangles(indexedDrawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        // Move all the points along the positive Y axis and draw another cube
+         AZ::Vector3 offset(0.0f, 4.0f, 0.0f);
+        for (int pointIndex = 0; pointIndex < NumCubePoints; ++pointIndex)
+        {
+            cubePoints[pointIndex] += offset;
+        }
+
+        // Translucent cube all one color 
+        indexedDrawArgs.m_colors = &RedAlpha;
+        auxGeom->DrawTriangles(indexedDrawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        // Move all the points along the positive Z axis and draw another cube
+        for (int pointIndex = 0; pointIndex < NumCubePoints; ++pointIndex)
+        {
+            cubePoints[pointIndex] += offset;
+        }
+
+        // Opaque cube with multiple colors
+        indexedDrawArgs.m_colors = cubeColors;
+        indexedDrawArgs.m_colorCount = NumCubePoints;
+        indexedDrawArgs.m_opacityType = AuxGeomDraw::OpacityType::Opaque;
+        auxGeom->DrawTriangles(indexedDrawArgs);
+
+        ///////////////////////////////////////////////////////////////////////
+        // Move all the points along the positive Z axis and draw another cube
+        for (int pointIndex = 0; pointIndex < NumCubePoints; ++pointIndex)
+        {
+            cubePoints[pointIndex] += offset;
+        }
+
+        // Translucent cube with multiple colors
+        indexedDrawArgs.m_opacityType = AuxGeomDraw::OpacityType::Translucent;
+        auxGeom->DrawTriangles(indexedDrawArgs);
+    }
+
+    void DrawShapes(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        // Draw a mixture of opaque and translucent shapes to test distance sorting of objects
+
+        const int numRows = 4;
+        AZ::Color opaqueColors[numRows] = { Red, Green, Blue, Yellow };
+        AZ::Color translucentColors[numRows] = { RedAlpha, GreenAlpha, BlueAlpha, YellowAlpha };
+
+        const int numDrawStyles = 4;
+        AuxGeomDraw::DrawStyle drawStyles[numDrawStyles] = { AuxGeomDraw::DrawStyle::Point, AuxGeomDraw::DrawStyle::Line, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DrawStyle::Shaded };
+
+        AZ::Vector3 direction[numRows] =
+        {
+            AZ::Vector3( 0.0f, -1.0f,  0.0f),
+            AZ::Vector3(-1.0f,  0.0f,  0.0f),
+            AZ::Vector3( 1.0f,  1.0f,  0.0f),
+            AZ::Vector3(-1.0f,  1.0f, -1.0f)
+        };
+
+        float radius = 1.0f;
+        float height = 1.0f;
+
+        float x = 5.0f;
+        float y = -8.0f;
+        float z = 2.0f;
+
+        Vector3 translucentOffset(2.0f, 0.0f, 0.5f);
+        Vector3 shapeOffset(0.0f, 0.0f, 3.0f);
+        Vector3 styleOffset(8.0f, 0.0f, 0.0f);
+
+        float colorYOffset = -8.0f;
+        auxGeom->SetPointSize(5.0f);
+
+        // Each row is drawn in a different color (colors changing along the Z axis)
+        // Within each row we draw every shape in all three draw styles, both opaque and translucent
+        for (int rowIndex = 0; rowIndex < numRows; ++rowIndex)
+        {
+            Vector3 basePosition = Vector3(x, y, z);
+
+            // Spread the draw style out along the X axis
+            for (int style = 0; style < numDrawStyles; ++style)
+            {
+                /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Draw Spheres
+                // Two spheres, one opaque, one translucent, One DepthTest On & DepthWrite off, One DepthTest Off & DepthWrite On
+                Vector3 shapePosition = basePosition;
+                auxGeom->DrawSphere(shapePosition, radius, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+                auxGeom->DrawSphere(shapePosition + 1.0f * translucentOffset, radius, translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off, AuxGeomDraw::FaceCullMode::Back);
+
+                // Two spheres, One DepthTest On & DepthWrite off, One DepthTest Off & DepthWrite On
+                shapePosition += shapeOffset;
+                auxGeom->DrawSphere(shapePosition, radius, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off, AuxGeomDraw::FaceCullMode::Back);
+                auxGeom->DrawSphere(shapePosition + translucentOffset, radius, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::Off, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+
+                // Three spheres, One no face cull, One front face cull, One back face cull
+                shapePosition += shapeOffset;
+                auxGeom->DrawSphere(shapePosition, radius, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::None);
+                auxGeom->DrawSphere(shapePosition + translucentOffset, radius, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Front);
+                auxGeom->DrawSphere(shapePosition + 2.0f * translucentOffset, radius, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+
+
+                /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Draw Cones
+                // Two cones, one opaque, one translucent
+                shapePosition += 2.0f * shapeOffset;
+                auxGeom->DrawCone(shapePosition, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+                auxGeom->DrawCone(shapePosition + translucentOffset, direction[rowIndex], radius, height, translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off, AuxGeomDraw::FaceCullMode::Back);
+
+                // Two cones, One DepthTest On & DepthWrite off, One DepthTest Off & DepthWrite On
+                shapePosition += shapeOffset;
+                auxGeom->DrawCone(shapePosition, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off, AuxGeomDraw::FaceCullMode::Back);
+                auxGeom->DrawCone(shapePosition + translucentOffset, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::Off, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+
+                // Three cones, One no face cull, One front face cull, One back face cull
+                shapePosition += shapeOffset;
+                auxGeom->DrawCone(shapePosition, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::None);
+                auxGeom->DrawCone(shapePosition + translucentOffset, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Front);
+                auxGeom->DrawCone(shapePosition + 2.0f * translucentOffset, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+
+
+                /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Draw Cylinders
+                // Two cylinders, one opaque, one translucent
+                shapePosition += 2.0f * shapeOffset;
+                auxGeom->DrawCylinder(shapePosition, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+                auxGeom->DrawCylinder(shapePosition + translucentOffset, direction[rowIndex], radius, height, translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off, AuxGeomDraw::FaceCullMode::Back);
+
+                // Two cylinders, One DepthTest On & DepthWrite off, One DepthTest Off & DepthWrite On
+                shapePosition += shapeOffset;
+                auxGeom->DrawCylinder(shapePosition, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off, AuxGeomDraw::FaceCullMode::Back);
+                auxGeom->DrawCylinder(shapePosition + translucentOffset, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::Off, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+
+                // Three cylinders, One no face cull, One front face cull, One back face cull
+                shapePosition += shapeOffset;
+                auxGeom->DrawCylinder(shapePosition, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::None);
+                auxGeom->DrawCylinder(shapePosition + translucentOffset, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Front);
+                auxGeom->DrawCylinder(shapePosition + 2.0f * translucentOffset, direction[rowIndex], radius, height, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::Back);
+
+                /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                basePosition += styleOffset;
+            }
+
+            // move along Z axis for next color
+            y += colorYOffset;
+            radius *= 0.75f;
+            height *= 1.25f;
+        }
+    }
+
+    void DrawBoxes(AZ::RPI::AuxGeomDrawPtr auxGeom, float x)
+    {
+        // Draw a mixture of opaque and translucent boxes
+        const int numRows = 4;
+        AZ::Color opaqueColors[numRows] = { Red, Green, Blue, Yellow };
+        AZ::Color translucentColors[numRows] = { RedAlpha, GreenAlpha, BlueAlpha, YellowAlpha };
+
+        const int numStyles = 4;
+        AuxGeomDraw::DrawStyle drawStyles[numStyles] = { AuxGeomDraw::DrawStyle::Point, AuxGeomDraw::DrawStyle::Line, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DrawStyle::Shaded };
+
+        // The size of the box for each row
+        AZ::Vector3 halfExtents[numRows] =
+        {
+            AZ::Vector3( 0.5f,  2.0f,  1.0f),
+            AZ::Vector3( 1.0f,  1.5f,  1.0f),
+            AZ::Vector3( 2.0f,  0.5f,  0.5f),
+            AZ::Vector3( 0.1f,  1.0f,  2.0f)
+        };
+
+        // The euler rotations for each row that has a rotation
+        AZ::Vector3 rotation[numRows] =
+        {
+            AZ::Vector3( 90.0f,  0.0f,  90.0f),
+            AZ::Vector3( 90.0f,  0.0f,   0.0f),
+            AZ::Vector3(  0.0f,  0.0f, -45.0f),
+            AZ::Vector3(  0.0f, 45.0f, -45.0f)
+        };
+
+        float y = 5.0f;
+        float z = 2.0f;
+
+        Vector3 translucentOffset(2.0f, 0.0f, 0.5f);
+        Vector3 typeOffset(0.0f, 0.0f, 3.0f);
+        Vector3 styleOffset(8.0f, 0.0f, 0.0f);
+
+        // each translucent box is positioned like its opaque partner but with this additional scale
+        Vector3 transScale(1.5f, 0.5f, 1.0f);
+
+        float colorYOffset = 8.0f;
+
+        auxGeom->SetPointSize(10.0f);
+
+        // Each row is drawn in a different color (colors changing along the Z axis)
+        // Within each row we draw every box type in all three draw styles, both opaque and translucent
+        for (int rowIndex = 0; rowIndex < numRows; ++rowIndex)
+        {
+            Vector3 basePosition = Vector3(x, y, z);
+
+            AZ::Transform rotationTransform;
+            rotationTransform.SetFromEulerDegrees(rotation[rowIndex]);
+            AZ::Quaternion rotationQuaternion = rotationTransform.GetRotation();
+            AZ::Matrix3x3 rotationMatrix = AZ::Matrix3x3::CreateFromTransform(rotationTransform);
+
+            // Spread the draw style out along the X axis
+            for (int style = 0; style < numStyles; ++style)
+            {
+                Vector3 boxPosition = basePosition;
+
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Layer 0: AABBs with no transform
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                AZ::Aabb aabb = AZ::Aabb::CreateCenterHalfExtents(basePosition, halfExtents[rowIndex]);
+                auxGeom->DrawAabb(aabb, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On);
+
+                AZ::Vector3 scaledHalfExtents = halfExtents[rowIndex] * transScale;
+                for (int index = 0; index < 3; ++index)
+                aabb = AZ::Aabb::CreateCenterHalfExtents(basePosition + translucentOffset, scaledHalfExtents);
+                auxGeom->DrawAabb(aabb, translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off);
+
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Layer 1: AABBs with transforms that rotate and scale
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+                AZ::Matrix3x4 transform = AZ::Matrix3x4::CreateFromMatrix3x3AndTranslation(rotationMatrix, basePosition + typeOffset);
+                aabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), halfExtents[rowIndex]);
+                auxGeom->DrawAabb(aabb, transform, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On);
+
+                // for the transparent version apply an additional scale to the transform
+                transform = AZ::Matrix3x4::CreateFromMatrix3x3AndTranslation(rotationMatrix, basePosition + typeOffset + translucentOffset);
+                transform.MultiplyByScale(transScale);
+                auxGeom->DrawAabb(aabb, transform, translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off);
+
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Layer 2: OBB with all position, rotation scale defined in the OBB itself
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+                // position the OOB using Obb::SetPosition and rotate it using Obb::SetAxis
+                AZ::Obb obb = AZ::Obb::CreateFromAabb(aabb);
+                obb.SetPosition(basePosition + 2 * typeOffset);
+                obb.SetRotation(rotationQuaternion);
+                auxGeom->DrawObb(obb, AZ::Matrix3x4::CreateIdentity(), opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On);
+
+                // for the transparent version apply an additional scale to the transform
+                obb.SetPosition(basePosition + 2 * typeOffset + translucentOffset);
+                for (int index = 0; index < 3; ++index)
+                {
+                    obb.SetHalfLength(index, obb.GetHalfLength(index) * transScale.GetElement(index));
+                }
+                auxGeom->DrawObb(obb, AZ::Matrix3x4::CreateIdentity(), translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On);
+
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Layer 3: OBB with rotation and scale defined in the OBB itself but position passed to DrawObb separately
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+                // position the OOB using position param to DrawObb, rotate it using  Obb::SetAxis
+                obb = AZ::Obb::CreateFromAabb(aabb);
+                obb.SetRotation(rotationQuaternion);
+                auxGeom->DrawObb(obb, basePosition + 3 * typeOffset, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On);
+
+                // For the translucent version apply the additional scale in the half extents
+                for (int index = 0; index < 3; ++index)
+                {
+                    obb.SetHalfLength(index, obb.GetHalfLength(index) * transScale.GetElement(index));
+                }
+                auxGeom->DrawObb(obb, basePosition + 3 * typeOffset + translucentOffset, translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off);
+
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+                // Layer 4: OBB with rotation and scale and translation defined in the transform passed to DrawObb
+                ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+                // position and rotate the OOB using transform param to DrawObb
+                obb = AZ::Obb::CreateFromAabb(aabb);
+
+                transform = AZ::Matrix3x4::CreateFromMatrix3x3AndTranslation(rotationMatrix, basePosition + 4 * typeOffset);
+                auxGeom->DrawObb(obb, transform, opaqueColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On);
+
+                // for the transparent version apply an additional scale to the transform
+                transform = AZ::Matrix3x4::CreateFromMatrix3x3AndTranslation(rotationMatrix, basePosition + 4 * typeOffset + translucentOffset);
+                transform.MultiplyByScale(transScale);
+                auxGeom->DrawObb(obb, transform, translucentColors[rowIndex], drawStyles[style], AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off);
+
+                basePosition += styleOffset;
+            }
+
+            // move along Z axis for next color
+            y += colorYOffset;
+        }
+    }
+
+    void DrawManyPrimitives(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        // Draw a grid of 300 x 200 quads (as triangle pairs - no shared verts)
+
+        const float y = 20.0f;
+        const float xOrigin = -30.0f;
+        const float zOrigin = 0.0f;
+        const float width = 0.1f;
+        const float height = 0.1f;
+
+        // we will draw 300 by 200 (60,000) quads as triangle pairs = 120,000 triangles = 360,000 vertices
+        const int widthInQuads = 300;
+        const int heightInQuads = 200;
+        AZ::RPI::AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+        drawArgs.m_vertCount = 3;
+        drawArgs.m_colorCount = 1;
+
+        for (int xIndex = 0; xIndex < widthInQuads; ++xIndex)
+        {
+            for (int zIndex = 0; zIndex < heightInQuads; ++zIndex)
+            {
+                const float xMin = xOrigin + xIndex * width;
+                const float xMax = xMin + width;
+                const float zMin = zOrigin + zIndex * height;
+                const float zMax = zMin + height;
+
+                AZ::Color color(static_cast<float>(xIndex) / (widthInQuads - 1), static_cast<float>(zIndex) / (heightInQuads - 1), 0.0f, 1.0f);
+                AZ::Vector3 verts[3] = {AZ::Vector3(xMin, y, zMax), AZ::Vector3(xMax, y, zMax), AZ::Vector3(xMax, y, zMin)};
+
+                drawArgs.m_verts = verts;
+                drawArgs.m_colors = &color;
+                auxGeom->DrawTriangles(drawArgs);
+
+                AZStd::swap(verts[0], verts[2]);
+                verts[1] = AZ::Vector3(xMin, y, zMin);
+                auxGeom->DrawTriangles(drawArgs);
+            }
+        }
+    }
+
+    void DrawDepthTestPrimitives(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        float width = 2.0f;
+        float left = -20.0f;
+        float right = left + width;
+        float bottom = -1.0f;
+        float top = bottom + width;
+        float spacing = 1.0f;
+        float yStart = -1.0f;
+        float y = yStart;
+
+        //Draw opaque cube using DrawTriangles
+        right = left + width;
+        float front = yStart;
+        float back = front + width;
+        const uint32_t NumCubePoints = 8;
+        AZ::Vector3 cubePoints[NumCubePoints] =
+        {
+            AZ::Vector3(left, front, top), AZ::Vector3(right, front, top), AZ::Vector3(right, front, bottom), AZ::Vector3(left, front, bottom),
+            AZ::Vector3(left, back, top), AZ::Vector3(right, back, top), AZ::Vector3(right, back, bottom), AZ::Vector3(left, back, bottom),
+        };
+        const uint32_t NumCubeIndicies = 36;
+        uint32_t cubeIndicies[NumCubeIndicies] =
+        {
+            0, 1, 2,  2, 3, 0,  // front face
+            4, 5, 6,  6, 7, 4,  // back face (no back-face culling)
+            0, 3, 7,  7, 4, 0,  // left
+            1, 2, 6,  6, 5, 1,  // right
+            0, 1, 5,  5, 4, 0,  // top
+            2, 3, 7,  7, 6, 2,  // bottom
+        };
+        ///////////////////////////////////////////////////////////////////////
+        // Opaque cube all one color, depth write & depth test on
+        AZ::RPI::AuxGeomDraw::AuxGeomDynamicIndexedDrawArguments drawArgs;
+        drawArgs.m_verts = cubePoints;
+        drawArgs.m_vertCount = NumCubePoints;
+        drawArgs.m_indices = cubeIndicies;
+        drawArgs.m_indexCount = NumCubeIndicies;
+        drawArgs.m_colors = &Red;
+        drawArgs.m_colorCount = 1;
+        drawArgs.m_depthTest = AuxGeomDraw::DepthTest::On;
+        drawArgs.m_depthWrite = AuxGeomDraw::DepthWrite::On;
+        auxGeom->DrawTriangles(drawArgs);
+
+        AZ::Vector3 offset = AZ::Vector3(-5.0f, 0.0f, 0.0f);
+        AZ::Vector3 scale = AZ::Vector3(1.0f, 3.0f, 3.0f);
+        for (int pointIndex = 0; pointIndex < NumCubePoints; ++pointIndex)
+        {
+            cubePoints[pointIndex] *= scale;
+            cubePoints[pointIndex] += offset;
+        }
+        ///////////////////////////////////////////////////////////////////////
+        // Opaque cube all one color, depth write off
+        drawArgs.m_colors = &Green;
+        drawArgs.m_depthTest = AuxGeomDraw::DepthTest::On;
+        drawArgs.m_depthWrite = AuxGeomDraw::DepthWrite::Off;
+        auxGeom->DrawTriangles(drawArgs);
+
+        offset = AZ::Vector3(-5.0f, 1.0f, 0.0f);
+        scale = AZ::Vector3(1.0f, 0.3333f, 0.3333f);
+        for (int pointIndex = 0; pointIndex < NumCubePoints; ++pointIndex)
+        {
+            cubePoints[pointIndex] *= scale;
+            cubePoints[pointIndex] += offset;
+        }
+        ///////////////////////////////////////////////////////////////////////
+        // Opaque cube all one color, depth Test off
+        drawArgs.m_colors = &Blue;
+        drawArgs.m_depthTest = AuxGeomDraw::DepthTest::Off;
+        drawArgs.m_depthWrite = AuxGeomDraw::DepthWrite::On;
+        auxGeom->DrawTriangles(drawArgs);
+
+        offset = AZ::Vector3(-5.0f, 1.0f, 0.0f);
+        for (int pointIndex = 0; pointIndex < NumCubePoints; ++pointIndex)
+        {
+            cubePoints[pointIndex] += offset;
+        }
+        ///////////////////////////////////////////////////////////////////////
+        // Opaque cube all one color, depth Test on, depth Write on
+        drawArgs.m_colors = &Yellow;
+        drawArgs.m_depthTest = AuxGeomDraw::DepthTest::On;
+        drawArgs.m_depthWrite = AuxGeomDraw::DepthWrite::On;
+        auxGeom->DrawTriangles(drawArgs);
+
+        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+        // Repeat with AABB shapes
+        float radius = width / 2.0f;
+        AZ::Vector3 basePosition = AZ::Vector3(-20.0f + radius, -7.0f, 0.0f); // adjust x pos by +radius to account for AABB's being centered relative to the coordinate.
+
+        AZ::Aabb aabb = AZ::Aabb::CreateCenterRadius(basePosition, radius);
+        auxGeom->DrawAabb(aabb, Red, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::None, -1);
+
+        aabb = AZ::Aabb::CreateCenterHalfExtents(basePosition + 1.0f * offset, AZ::Vector3(radius, 3.0f * radius, 3.0f * radius));
+        auxGeom->DrawAabb(aabb, Green, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::Off, AuxGeomDraw::FaceCullMode::None, -1);
+
+        aabb = AZ::Aabb::CreateCenterRadius(basePosition + 2.0f * offset, radius);
+        auxGeom->DrawAabb(aabb, Blue, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DepthTest::Off, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::None, -1);
+
+        aabb = AZ::Aabb::CreateCenterRadius(basePosition + 3.0f * offset, radius);
+        auxGeom->DrawAabb(aabb, Yellow, AuxGeomDraw::DrawStyle::Solid, AuxGeomDraw::DepthTest::On, AuxGeomDraw::DepthWrite::On, AuxGeomDraw::FaceCullMode::None, -1);
+    }
+
+    void Draw2DWireRect(AZ::RPI::AuxGeomDrawPtr auxGeom, const AZ::Color& color, float z)
+    {
+        float vertOffset = z * 0.45f;
+        AZ::Vector3 verts[4] = {
+            AZ::Vector3(0.5f - vertOffset, 0.5f - vertOffset, z), 
+            AZ::Vector3(0.5f - vertOffset, 0.5f + vertOffset, z), 
+            AZ::Vector3(0.5f + vertOffset, 0.5f + vertOffset, z), 
+            AZ::Vector3(0.5f + vertOffset, 0.5f - vertOffset, z) };
+        AZ::Color Red = AZ::Color(1.0f, 0.0f, 0.0f, 1.0f);
+        int32_t viewProjOverrideIndex = auxGeom->GetOrAdd2DViewProjOverride();
+        AZ::RPI::AuxGeomDraw::AuxGeomDynamicDrawArguments drawArgs;
+        drawArgs.m_verts = verts;
+        drawArgs.m_vertCount = 4;
+        drawArgs.m_colors = &color;
+        drawArgs.m_colorCount = 1;
+        drawArgs.m_viewProjectionOverrideIndex = viewProjOverrideIndex;
+        auxGeom->DrawPolylines(drawArgs, AZ::RPI::AuxGeomDraw::PolylineEnd::Closed);
+    }
+}

+ 44 - 0
Gem/Code/Source/AuxGeomSharedDrawFunctions.h

@@ -0,0 +1,44 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <Atom/RPI.Public/AuxGeom/AuxGeomDraw.h>
+
+namespace AtomSampleViewer
+{
+    // Create some semi-transparent colors
+    extern const AZ::Color BlackAlpha;
+    extern const AZ::Color WhiteAlpha;
+
+    extern const AZ::Color RedAlpha;
+    extern const AZ::Color GreenAlpha;
+    extern const AZ::Color BlueAlpha;
+
+    extern const AZ::Color YellowAlpha;
+    extern const AZ::Color CyanAlpha;
+    extern const AZ::Color MagentaAlpha;
+
+    extern const AZ::Color LightGray;
+    extern const AZ::Color DarkGray;
+
+    void DrawBackgroundBox(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void DrawThreeGridsOfPoints(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void DrawAxisLines(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void DrawLines(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void DrawTriangles(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void DrawShapes(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void DrawBoxes(AZ::RPI::AuxGeomDrawPtr auxGeom, float x = 10.0f);
+    void DrawManyPrimitives(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void DrawDepthTestPrimitives(AZ::RPI::AuxGeomDrawPtr auxGeom);
+    void Draw2DWireRect(AZ::RPI::AuxGeomDrawPtr auxGeom, const AZ::Color& color, float z = 0.99f);
+} // namespace AtomSampleViewer

+ 656 - 0
Gem/Code/Source/BistroBenchmarkComponent.cpp

@@ -0,0 +1,656 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <BistroBenchmarkComponent.h>
+
+#include <Atom/RHI/Device.h>
+#include <Atom/RHI/Factory.h>
+
+#include <Atom/Feature/ImGui/SystemBus.h>
+
+#include <Atom/Feature/Utils/FrameCaptureBus.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/Serialization/Utils.h>
+#include <AzCore/std/sort.h>
+
+#include <AzFramework/IO/LocalFileIO.h>
+#include <AzFramework/Components/TransformComponent.h>
+
+#include <Utils/Utils.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+
+#include <ctime>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    void BistroBenchmarkComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<BistroBenchmarkComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+        BistroBenchmarkComponent::LoadBenchmarkData::Reflect(context);
+        BistroBenchmarkComponent::LoadBenchmarkData::FileLoadedData::Reflect(context);
+
+        BistroBenchmarkComponent::RunBenchmarkData::Reflect(context);
+    }
+
+    void BistroBenchmarkComponent::LoadBenchmarkData::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<BistroBenchmarkComponent::LoadBenchmarkData>()
+                ->Version(0)
+                ->Field("Name", &BistroBenchmarkComponent::LoadBenchmarkData::m_name)
+                ->Field("TimeInSeconds", &BistroBenchmarkComponent::LoadBenchmarkData::m_timeInSeconds)
+                ->Field("TotalMBLoaded", &BistroBenchmarkComponent::LoadBenchmarkData::m_totalMBLoaded)
+                ->Field("MB/s", &BistroBenchmarkComponent::LoadBenchmarkData::m_mbPerSec)
+                ->Field("# of Files Loaded", &BistroBenchmarkComponent::LoadBenchmarkData::m_numFilesLoaded)
+                ->Field("FilesLoaded", &BistroBenchmarkComponent::LoadBenchmarkData::m_filesLoaded)
+                ;
+        }
+    }
+
+    void BistroBenchmarkComponent::LoadBenchmarkData::FileLoadedData::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<BistroBenchmarkComponent::LoadBenchmarkData::FileLoadedData>()
+                ->Version(0)
+                ->Field("RelativePath", &BistroBenchmarkComponent::LoadBenchmarkData::FileLoadedData::m_relativePath)
+                ->Field("BytesLoaded", &BistroBenchmarkComponent::LoadBenchmarkData::FileLoadedData::m_bytesLoaded)
+                ;
+        }
+    }
+
+    void BistroBenchmarkComponent::RunBenchmarkData::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<BistroBenchmarkComponent::RunBenchmarkData>()
+                ->Version(0)
+                ->Field("Name", &BistroBenchmarkComponent::RunBenchmarkData::m_name)
+                ->Field("FrameCount", &BistroBenchmarkComponent::RunBenchmarkData::m_frameCount)
+                ->Field("TimeToFirstFrame", &BistroBenchmarkComponent::RunBenchmarkData::m_timeToFirstFrame)
+                ->Field("TimeInSeconds", &BistroBenchmarkComponent::RunBenchmarkData::m_timeInSeconds)
+                ->Field("AverageFrameTime", &BistroBenchmarkComponent::RunBenchmarkData::m_averageFrameTime)
+                ->Field("50% of FrameTimes Under", &BistroBenchmarkComponent::RunBenchmarkData::m_50pFramesUnder)
+                ->Field("90% of FrameTimes Under", &BistroBenchmarkComponent::RunBenchmarkData::m_90pFramesUnder)
+                ->Field("MinFrameTime", &BistroBenchmarkComponent::RunBenchmarkData::m_minFrameTime)
+                ->Field("MaxFrameTime", &BistroBenchmarkComponent::RunBenchmarkData::m_maxFrameTime)
+                ->Field("AverageFrameRate", &BistroBenchmarkComponent::RunBenchmarkData::m_averageFrameRate)
+                ->Field("MinFrameRate", &BistroBenchmarkComponent::RunBenchmarkData::m_minFrameRate)
+                ->Field("MaxFrameRate", &BistroBenchmarkComponent::RunBenchmarkData::m_maxFrameRate)
+                ;
+        }
+    }
+
+    void BistroBenchmarkComponent::Activate()
+    {
+        auto traceLevel = AZ::RPI::AssetUtils::TraceLevel::Assert;
+        m_bistroExteriorAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>
+            ("Objects/Bistro/Bistro_Research_Exterior.azmodel", traceLevel);
+        m_bistroInteriorAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>
+            ("Objects/Bistro/Bistro_Research_Interior.azmodel", traceLevel);
+
+        m_bistroExteriorMeshHandle = GetMeshFeatureProcessor()->AcquireMesh(m_bistroExteriorAsset);
+        m_bistroInteriorMeshHandle = GetMeshFeatureProcessor()->AcquireMesh(m_bistroInteriorAsset);
+
+        // rotate the entities 180 degrees about Z (the vertical axis)
+        // This makes it consistent with how it was positioned in the world when the world was Y-up.
+        GetMeshFeatureProcessor()->SetTransform(m_bistroExteriorMeshHandle, AZ::Transform::CreateRotationZ(AZ::Constants::Pi));
+        GetMeshFeatureProcessor()->SetTransform(m_bistroInteriorMeshHandle, AZ::Transform::CreateRotationZ(AZ::Constants::Pi));
+
+        BenchmarkLoadStart();
+
+        // Capture screenshots on specific frames.
+        const AzFramework::CommandLine* commandLine = nullptr;
+        AzFramework::ApplicationRequests::Bus::BroadcastResult(commandLine, &AzFramework::ApplicationRequests::GetCommandLine);
+
+        static const char* screenshotFlagName = "screenshot";
+        if (commandLine && commandLine->HasSwitch(screenshotFlagName))
+        {
+            size_t capturesCount = commandLine->GetNumSwitchValues(screenshotFlagName);
+            for (size_t i = 0; i < capturesCount; ++i)
+            {
+                AZStd::string frameNumberStr = commandLine->GetSwitchValue(screenshotFlagName, i);
+                uint64_t frameNumber = strtoull(frameNumberStr.begin(), nullptr, 0);
+                if (frameNumber > 0)
+                {
+                    m_framesToCapture.push_back(frameNumber);
+                }
+            }
+            AZStd::sort(m_framesToCapture.begin(), m_framesToCapture.end(), AZStd::greater<uint64_t>());
+        }
+
+        AZ::TickBus::Handler::BusConnect();
+
+        const char* engineRoot = nullptr;
+        AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot);
+        if (engineRoot)
+        {
+            AzFramework::StringFunc::Path::Join(engineRoot, "Screenshots", m_screenshotFolder, true, false);
+        }
+
+        auto defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        m_directionalLightFeatureProcessor = defaultScene->GetFeatureProcessor<AZ::Render::DirectionalLightFeatureProcessorInterface>();
+
+        const auto handle = m_directionalLightFeatureProcessor->AcquireLight();
+
+        AZ::Vector3 sunDirection = AZ::Vector3(1.0f, -1.0f, -3.0f);
+        sunDirection.Normalize();
+        m_directionalLightFeatureProcessor->SetDirection(handle, sunDirection);
+
+        AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Lux> sunColor(AZ::Color(1.0f, 1.0f, 0.97f, 1.0f) * 20.f);
+        m_directionalLightFeatureProcessor->SetRgbIntensity(handle, sunColor);
+        m_directionalLightFeatureProcessor->SetCascadeCount(handle, 4);
+        m_directionalLightFeatureProcessor->SetShadowmapSize(handle, AZ::Render::ShadowmapSizeNamespace::ShadowmapSize::Size2048);
+        m_directionalLightFeatureProcessor->SetViewFrustumCorrectionEnabled(handle, true);
+        m_directionalLightFeatureProcessor->SetShadowFilterMethod(handle, AZ::Render::ShadowFilterMethod::EsmPcf);
+        m_directionalLightFeatureProcessor->SetShadowBoundaryWidth(handle, 0.03);
+        m_directionalLightFeatureProcessor->SetPredictionSampleCount(handle, 4);
+        m_directionalLightFeatureProcessor->SetFilteringSampleCount(handle, 16);
+        m_directionalLightFeatureProcessor->SetGroundHeight(handle, 0.f);
+        m_directionalLightHandle = handle;
+
+        // Enable physical sky
+        m_skyboxFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::SkyBoxFeatureProcessorInterface>(GetEntityContextId());
+        AZ_Assert(m_skyboxFeatureProcessor, "BistroBenchmarkComponent unable to find SkyBoxFeatureProcessorInterface.");
+        m_skyboxFeatureProcessor->SetSkyboxMode(AZ::Render::SkyBoxMode::PhysicalSky);
+        m_skyboxFeatureProcessor->Enable(true);
+
+        float azimuth = atan2(-sunDirection.GetZ(), -sunDirection.GetX());
+        float altitude = asin(-sunDirection.GetY() / sunDirection.GetLength());
+
+        m_skyboxFeatureProcessor->SetSunPosition(azimuth, altitude);
+
+        // Create IBL
+        m_defaultIbl.Init(AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get());
+        m_defaultIbl.SetExposure(-3.0f);
+    }
+
+    void BistroBenchmarkComponent::Deactivate()
+    {
+        AZ::TickBus::Handler::BusDisconnect();
+
+        // If there are any assets that haven't finished loading yet, and thus haven't been disconnected, disconnect now.
+        AZ::Data::AssetBus::MultiHandler::BusDisconnect();
+        
+        m_defaultIbl.Reset();
+
+        m_skyboxFeatureProcessor->Enable(false);
+
+        GetMeshFeatureProcessor()->ReleaseMesh(m_bistroExteriorMeshHandle);
+        GetMeshFeatureProcessor()->ReleaseMesh(m_bistroInteriorMeshHandle);
+
+        m_directionalLightFeatureProcessor->ReleaseLight(m_directionalLightHandle);
+        m_directionalLightFeatureProcessor = nullptr;
+    }
+
+    void BistroBenchmarkComponent::OnTick(float deltaTime, AZ::ScriptTimePoint timePoint)
+    {
+        AZ_PROFILE_DATAPOINT(AZ::Debug::ProfileCategory::AzRender, deltaTime, "Frame Time");
+
+        // Camera Configuration
+        {
+            Camera::Configuration config;
+            Camera::CameraRequestBus::EventResult(
+                config,
+                GetCameraEntityId(),
+                &Camera::CameraRequestBus::Events::GetCameraConfiguration);
+            m_directionalLightFeatureProcessor->SetCameraConfiguration(
+                m_directionalLightHandle,
+                config);
+        }
+
+        // Camera Transform
+        {
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            AZ::TransformBus::EventResult(
+                transform,
+                GetCameraEntityId(),
+                &AZ::TransformBus::Events::GetWorldTM);
+            m_directionalLightFeatureProcessor->SetCameraTransform(
+                m_directionalLightHandle, transform);
+        }
+
+        m_currentTimePointInSeconds = timePoint.GetSeconds();
+
+        if (m_bistroExteriorLoaded == false || m_bistroInteriorLoaded == false)
+        {
+            DisplayLoadingDialog();
+        }
+        else
+        {
+            if (m_frameCount >= m_exteriorPath.back().m_framePoint)
+            {
+                if (m_endBenchmarkCapture)
+                {
+                    BenchmarkRunEnd();
+
+                    m_endBenchmarkCapture = false;
+                }
+
+                DisplayResults();
+            }
+            else
+            {
+                if (m_startBenchmarkCapture)
+                {
+                    BenchmarkRunStart();
+
+                    m_startBenchmarkCapture = false;
+                }
+
+                bool screenshotRequested = false;
+                // Check if a screenshot was requested for this frame. Loop in case there were multiple requests for the same frame.
+                while (m_framesToCapture.size() > 0 && m_frameCount == m_framesToCapture.back())
+                {
+                    screenshotRequested = true;
+                    m_framesToCapture.pop_back();
+                }
+
+                if (screenshotRequested)
+                {
+                    AZStd::string filePath;
+                    AZStd::string fileName = AZStd::string::format("screenshot_bistro_%llu.dds", m_frameCount);
+                    AzFramework::StringFunc::Path::Join(m_screenshotFolder.c_str(), fileName.c_str(), filePath, true, false);
+                    AZ::Render::FrameCaptureRequestBus::Broadcast(&AZ::Render::FrameCaptureRequestBus::Events::CaptureScreenshot, filePath);
+                }
+
+                if (m_frameCount == 1)
+                {
+                    m_timeToFirstFrame = m_currentTimePointInSeconds - m_benchmarkStartTimePoint;
+                } 
+
+                CollectRunBenchmarkData(deltaTime, timePoint);
+
+                // Find current working camera point
+                size_t currentCameraPointIndex = 0;
+                while (m_exteriorPath[currentCameraPointIndex + 1].m_framePoint < m_frameCount && currentCameraPointIndex < (m_exteriorPath.size() - 2))
+                {
+                    currentCameraPointIndex++;
+                }
+                const CameraPathPoint& cameraPathPoint = m_exteriorPath[currentCameraPointIndex];
+                const CameraPathPoint& nextCameraPathPoint = m_exteriorPath[currentCameraPointIndex + 1];
+
+                // Lerp to get intermediate position
+                {
+                    const float percentToNextPoint = static_cast<float>((m_frameCount - cameraPathPoint.m_framePoint)) / static_cast<float>(nextCameraPathPoint.m_framePoint - cameraPathPoint.m_framePoint);
+
+                    const AZ::Vector3 position = cameraPathPoint.m_position.Lerp(nextCameraPathPoint.m_position, percentToNextPoint);
+                    const AZ::Vector3 target = cameraPathPoint.m_target.Lerp(nextCameraPathPoint.m_target, percentToNextPoint);
+                    const AZ::Vector3 up = cameraPathPoint.m_up.Lerp(nextCameraPathPoint.m_up, percentToNextPoint);
+
+                    const AZ::Transform transform = AZ::Transform::CreateLookAt(position, target, AZ::Transform::Axis::YPositive);
+
+                    // Apply transform
+                    AZ::TransformBus::Event(GetCameraEntityId(), &AZ::TransformBus::Events::SetWorldTM, transform);
+                }
+
+                m_frameCount++;
+            }
+        }
+    }
+
+    void BistroBenchmarkComponent::OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset)
+    {
+        if (asset.GetId() == m_bistroExteriorAsset.GetId())
+        {
+            m_bistroExteriorLoaded = true;
+        }
+        else if (asset.GetId() == m_bistroInteriorAsset.GetId())
+        {
+            m_bistroInteriorLoaded = true;
+        }
+
+        // Benchmark the count and total size of files loaded
+        static double invBytesToMB = 1 / (1024.0 * 1024.0);
+
+        AZ::Data::AssetInfo info;
+        AZ::Data::AssetCatalogRequestBus::BroadcastResult(info, &AZ::Data::AssetCatalogRequests::GetAssetInfoById, asset.GetId());
+
+        LoadBenchmarkData::FileLoadedData fileLoadedData;
+        fileLoadedData.m_relativePath = info.m_relativePath;
+        fileLoadedData.m_bytesLoaded = info.m_sizeBytes;
+
+        m_currentLoadBenchmarkData.m_totalMBLoaded += static_cast<double>(info.m_sizeBytes) * invBytesToMB;
+        m_currentLoadBenchmarkData.m_filesLoaded.emplace_back(AZStd::move(fileLoadedData));
+
+        if (m_bistroExteriorLoaded && m_bistroInteriorLoaded)
+        {
+            BenchmarkLoadEnd();
+        }
+
+        AZ_PROFILE_DATAPOINT(AZ::Debug::ProfileCategory::AzRender, m_currentLoadBenchmarkData.m_totalMBLoaded, "MB Loaded Off Disk");
+    }
+
+    void BistroBenchmarkComponent::BenchmarkLoadStart()
+    {
+        AZStd::vector<AZ::Data::AssetId> unloadedAssetsInCatalog;
+
+        unloadedAssetsInCatalog.push_back(m_bistroExteriorAsset.GetId());
+        unloadedAssetsInCatalog.push_back(m_bistroInteriorAsset.GetId());
+
+        // Get a vector of all assets that haven't been loaded
+        auto startCB = []() {};
+        auto enumerateCB = [&unloadedAssetsInCatalog](const AZ::Data::AssetId id, [[maybe_unused]] const AZ::Data::AssetInfo& assetInfo)
+        {
+            // Don't "get" the asset and load it, we just want to query its status
+            AZ::Data::Asset<AZ::Data::AssetData> asset = AZ::Data::AssetManager::Instance().FindAsset(id, AZ::Data::AssetLoadBehavior::PreLoad);
+
+            if (asset.GetData() == nullptr || asset.GetStatus() == AZ::Data::AssetData::AssetStatus::NotLoaded)
+            {
+                unloadedAssetsInCatalog.push_back(id);
+            }
+        };
+        auto endCB = []() {};
+
+        AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets, startCB, enumerateCB, endCB);
+
+        // Connect specifically to all assets in the catalog that haven't been loaded yet
+        // Otherwise if we just connect to *every* asset the ones that have already been loaded
+        // will still trigger OnAssetReady events
+        for (const AZ::Data::AssetId& id : unloadedAssetsInCatalog)
+        {
+            // OnAssetReady will keep track of number and size of all the files that are loaded during this benchmark
+            AZ::Data::AssetBus::MultiHandler::BusConnect(id);
+        }
+
+        m_currentLoadBenchmarkData = LoadBenchmarkData();
+        m_currentLoadBenchmarkData.m_name = "Bistro Load";
+
+        Utils::ToggleRadTMCapture();
+        m_benchmarkStartTimePoint = static_cast<double>(AZStd::GetTimeUTCMilliSecond());
+    }
+
+    void BistroBenchmarkComponent::FinalizeLoadBenchmarkData()
+    {
+        m_currentLoadBenchmarkData.m_timeInSeconds = (static_cast<double>(AZStd::GetTimeUTCMilliSecond()) - m_benchmarkStartTimePoint) / 1000.0f;
+        m_currentLoadBenchmarkData.m_mbPerSec = m_currentLoadBenchmarkData.m_totalMBLoaded / m_currentLoadBenchmarkData.m_timeInSeconds;
+        m_currentLoadBenchmarkData.m_numFilesLoaded = m_currentLoadBenchmarkData.m_filesLoaded.size();
+    }
+
+    void BistroBenchmarkComponent::BenchmarkLoadEnd()
+    {
+        AZ::Data::AssetBus::MultiHandler::BusDisconnect();
+
+        Utils::ToggleRadTMCapture();
+
+        FinalizeLoadBenchmarkData();
+
+        const AZStd::string unresolvedPath = AZStd::string::format("@user@/benchmarks/bistroLoad_%ld.xml", time(0));
+        char bistroLoadBenchmarkDataFilePath[AZ_MAX_PATH_LEN] = { 0 };
+        AZ::IO::FileIOBase::GetInstance()->ResolvePath(unresolvedPath.c_str(), bistroLoadBenchmarkDataFilePath, AZ_MAX_PATH_LEN);
+
+        if (!AZ::Utils::SaveObjectToFile(bistroLoadBenchmarkDataFilePath, AZ::DataStream::ST_XML, &m_currentLoadBenchmarkData))
+        {
+            AZ_Error("BistroBenchmarkComponent", false, "Failed to save bistro benchmark load data to file %s", bistroLoadBenchmarkDataFilePath);
+        }
+    }
+
+    void BistroBenchmarkComponent::BenchmarkRunStart()
+    {
+        m_currentRunBenchmarkData = RunBenchmarkData();
+        m_currentRunBenchmarkData.m_name = "Bistro Run";
+
+        Utils::ToggleRadTMCapture();
+        m_benchmarkStartTimePoint = m_currentTimePointInSeconds;
+    }
+
+    void BistroBenchmarkComponent::CollectRunBenchmarkData(float deltaTime, AZ::ScriptTimePoint timePoint)
+    {
+        const float dtInMS = deltaTime * 1000.0f;
+
+        m_currentRunBenchmarkData.m_frameTimes.push_back(dtInMS);
+        m_currentRunBenchmarkData.m_frameCount++;
+        m_currentRunBenchmarkData.m_timeInSeconds = timePoint.GetSeconds() - m_benchmarkStartTimePoint;
+        if (dtInMS < m_currentRunBenchmarkData.m_minFrameTime)
+        {
+            m_currentRunBenchmarkData.m_minFrameTime = dtInMS;
+        }
+        if (dtInMS > m_currentRunBenchmarkData.m_maxFrameTime)
+        {
+            m_currentRunBenchmarkData.m_maxFrameTime = dtInMS;
+        }
+        if (m_frameCount == 1)
+        {
+            m_currentRunBenchmarkData.m_timeToFirstFrame = m_timeToFirstFrame;
+        }
+    }
+
+    void BistroBenchmarkComponent::FinalizeRunBenchmarkData()
+    {
+        m_currentRunBenchmarkData.m_averageFrameTime =
+            (m_currentRunBenchmarkData.m_timeInSeconds / m_currentRunBenchmarkData.m_frameCount) * 1000.0f;
+
+        // Need to sort the frame times so we can find the 50th and 90th percentile
+        AZStd::vector<float> sortedFrameTimes = m_currentRunBenchmarkData.m_frameTimes;
+        AZStd::sort(sortedFrameTimes.begin(), sortedFrameTimes.end());
+
+        const size_t frameTimeCount = m_currentRunBenchmarkData.m_frameTimes.size();
+
+        const bool evenNumberOfFrames = frameTimeCount & 1;
+        if (!evenNumberOfFrames)
+        {
+            const size_t medianIndex = frameTimeCount / 2;
+
+            m_currentRunBenchmarkData.m_50pFramesUnder = sortedFrameTimes[medianIndex];
+        }
+        else
+        {
+            const size_t medianIndex1 = frameTimeCount / 2;
+            const size_t medianIndex2 = medianIndex1 + 1;
+
+            const float median = (sortedFrameTimes[medianIndex1] + sortedFrameTimes[medianIndex2]) / 2.0f;
+
+            m_currentRunBenchmarkData.m_50pFramesUnder = median;
+        }
+
+        const float p90Indexf = ceilf(static_cast<float>(frameTimeCount) * .9f);
+        const size_t p90Index = static_cast<size_t>(p90Indexf);
+
+        m_currentRunBenchmarkData.m_90pFramesUnder = sortedFrameTimes[p90Index];
+        m_currentRunBenchmarkData.m_timeToFirstFrame = m_timeToFirstFrame;
+
+        float averageFrameRate = 0.0f;
+        for (auto frame : m_currentRunBenchmarkData.m_frameTimes)
+        {
+            float frameRate = 1.0f / (frame / 1000);
+            if (frameRate > m_currentRunBenchmarkData.m_maxFrameRate)
+            {
+                m_currentRunBenchmarkData.m_maxFrameRate = frameRate;
+            }
+            if (frameRate < m_currentRunBenchmarkData.m_minFrameRate)
+            {
+                m_currentRunBenchmarkData.m_minFrameRate = frameRate;
+            }
+            m_currentRunBenchmarkData.m_frameRates.push_back(frameRate);
+            averageFrameRate += frameRate;
+        }
+        m_currentRunBenchmarkData.m_averageFrameRate = averageFrameRate / m_currentRunBenchmarkData.m_frameRates.size();
+    }
+
+    void BistroBenchmarkComponent::BenchmarkRunEnd()
+    {
+        Utils::ToggleRadTMCapture();
+
+        FinalizeRunBenchmarkData();
+
+        const AZStd::string unresolvedPath = AZStd::string::format("@user@/benchmarks/bistroRun_%ld.xml", time(0));
+
+        char bistroRunBenchmarkDataFilePath[AZ_MAX_PATH_LEN] = { 0 };
+        AZ::IO::FileIOBase::GetInstance()->ResolvePath(unresolvedPath.c_str(), bistroRunBenchmarkDataFilePath, AZ_MAX_PATH_LEN);
+
+        if (!AZ::Utils::SaveObjectToFile(bistroRunBenchmarkDataFilePath, AZ::DataStream::ST_XML, &m_currentRunBenchmarkData))
+        {
+            AZ_Error("BistroBenchmarkComponent", false, "Failed to save bistro benchmark run data to file %s", bistroRunBenchmarkDataFilePath);
+        }
+
+    }
+
+    void BistroBenchmarkComponent::DisplayLoadingDialog()
+    {
+        const ImGuiWindowFlags windowFlags =
+            ImGuiWindowFlags_NoCollapse |
+            ImGuiWindowFlags_NoResize |
+            ImGuiWindowFlags_NoMove;
+
+        AzFramework::NativeWindowHandle windowHandle = nullptr;
+        AzFramework::WindowSystemRequestBus::BroadcastResult(
+            windowHandle,
+            &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle);
+
+        AzFramework::WindowSize windowSize;
+        AzFramework::WindowRequestBus::EventResult(
+            windowSize,
+            windowHandle,
+            &AzFramework::WindowRequestBus::Events::GetClientAreaSize);
+
+        const float loadingWindowWidth = 225.0f;
+        const float loadingWindowHeight = 65.0f;
+
+        const float halfLoadingWindowWidth = loadingWindowWidth * 0.5f;
+        const float halfLoadingWindowHeight = loadingWindowHeight * 0.5f;
+
+        const float halfWindowWidth = windowSize.m_width * 0.5f;
+        const float halfWindowHeight = windowSize.m_height * 0.5f;
+
+        ImGui::SetNextWindowPos(ImVec2(halfWindowWidth - halfLoadingWindowWidth, halfWindowHeight - halfLoadingWindowHeight));
+        ImGui::SetNextWindowSize(ImVec2(loadingWindowWidth, loadingWindowHeight));
+
+        if (ImGui::Begin("Loading", nullptr, windowFlags))
+        {
+            const size_t loadingIndicatorSize = static_cast<size_t>(fmod(m_currentTimePointInSeconds, 2.0) / 0.5);
+            char* loadingIndicator = new char[loadingIndicatorSize + 1];
+            memset(loadingIndicator, '.', loadingIndicatorSize);
+            loadingIndicator[loadingIndicatorSize] = '\0';
+
+            if (m_bistroInteriorLoaded)
+            {
+                ImGui::Text("Bistro Exterior: Loaded!");
+            }
+            else
+            {
+                ImGui::Text("Bistro Exterior: Loading%s", loadingIndicator);
+            }
+
+            if (m_bistroInteriorLoaded)
+            {
+                ImGui::Text("Bistro Interior: Loaded!");
+            }
+            else
+            {
+                ImGui::Text("Bistro Interior: Loading%s", loadingIndicator);
+            }
+
+            delete[] loadingIndicator;
+        }
+        ImGui::End();
+    }
+
+    void BistroBenchmarkComponent::DisplayResults()
+    {
+        AzFramework::NativeWindowHandle windowHandle = nullptr;
+        AzFramework::WindowSystemRequestBus::BroadcastResult(
+            windowHandle,
+            &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle);
+
+        AzFramework::WindowSize windowSize;
+        AzFramework::WindowRequestBus::EventResult(
+            windowSize,
+            windowHandle,
+            &AzFramework::WindowRequestBus::Events::GetClientAreaSize);
+
+        const float halfWindowWidth = windowSize.m_width * 0.5f;
+        const float halfWindowHeight = windowSize.m_height * 0.5f;
+
+        const float frameTimeWindowWidth = static_cast<float>(windowSize.m_width);
+        const float frameTimeWindowHeight = 200.0f;
+
+        if (m_firstResultsDisplay)
+        {
+            ImGui::SetNextWindowPos(ImVec2(0.0f, windowSize.m_height - frameTimeWindowHeight));
+            ImGui::SetNextWindowSize(ImVec2(frameTimeWindowWidth, frameTimeWindowHeight));
+        }
+
+        if (ImGui::Begin("Frame Times"))
+        {
+            ImGui::PlotHistogram("##FrameTimes",
+                m_currentRunBenchmarkData.m_frameTimes.data(),
+                static_cast<int32_t>(m_currentRunBenchmarkData.m_frameTimes.size()),
+                0, nullptr,
+                0,
+                m_currentRunBenchmarkData.m_90pFramesUnder * 2.0f,
+                ImGui::GetContentRegionAvail());
+        }
+        ImGui::End();
+
+        const float resultWindowWidth = 500.0f;
+        const float resultWindowHeight = 250.0f;
+
+        const float halfResultWindowWidth = resultWindowWidth * 0.5f;
+        const float halfResultWindowHeight = resultWindowHeight * 0.5f;
+
+        if (m_firstResultsDisplay)
+        {
+            ImGui::SetNextWindowPos(ImVec2(halfWindowWidth - halfResultWindowWidth,
+                halfWindowHeight - halfResultWindowHeight));
+            ImGui::SetNextWindowSize(ImVec2(resultWindowWidth, resultWindowHeight));
+
+            m_firstResultsDisplay = false;
+        }
+
+        if (ImGui::Begin("Results"))
+        {
+            ImGui::Columns(2);
+            
+            ImGui::Text("Load");
+            ImGui::NextColumn();
+            ImGui::Text("Run");
+            ImGui::Separator();
+
+            ImGui::NextColumn();
+
+            ImGui::Text("File Count: %llu", m_currentLoadBenchmarkData.m_numFilesLoaded);
+            ImGui::Text("Total Time: %f seconds", m_currentLoadBenchmarkData.m_timeInSeconds);
+            ImGui::Text("Loaded: %f MB", m_currentLoadBenchmarkData.m_totalMBLoaded);
+            ImGui::Text("Throughput: %f MB/s", m_currentLoadBenchmarkData.m_mbPerSec);
+
+            ImGui::NextColumn();
+
+            ImGui::Text("Frame Count: %llu", m_currentRunBenchmarkData.m_frameCount);
+            ImGui::Text("Total Time: %f seconds", m_currentRunBenchmarkData.m_timeInSeconds);
+            ImGui::Text("Time to First Frame: %f ms", m_currentRunBenchmarkData.m_timeToFirstFrame);
+            ImGui::Text("Average Frame Time: %f ms", m_currentRunBenchmarkData.m_averageFrameTime);
+            ImGui::Text("50%% Frames Under: %f ms", m_currentRunBenchmarkData.m_50pFramesUnder);
+            ImGui::Text("90%% Frames Under: %f ms", m_currentRunBenchmarkData.m_90pFramesUnder);
+            ImGui::Text("Min Frame Time: %f ms", m_currentRunBenchmarkData.m_minFrameTime);
+            ImGui::Text("Max Frame Time: %f ms", m_currentRunBenchmarkData.m_maxFrameTime);
+            ImGui::Text("Average Frame Rate: %f Hz", m_currentRunBenchmarkData.m_averageFrameRate);
+            ImGui::Text("Min Frame Rate: %f Hz", m_currentRunBenchmarkData.m_minFrameRate);
+            ImGui::Text("Max Frame Rate: %f Hz", m_currentRunBenchmarkData.m_maxFrameRate);
+        }
+        ImGui::Columns(1);
+        ImGui::End();
+    }
+} // namespace AtomSampleViewer

+ 192 - 0
Gem/Code/Source/BistroBenchmarkComponent.h

@@ -0,0 +1,192 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <AtomCore/Instance/InstanceId.h>
+
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/Math/Vector3.h>
+#include <AzCore/std/string/string_view.h>
+
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+
+#include <Utils/Utils.h>
+
+struct ImGuiContext;
+
+namespace AtomSampleViewer
+{
+    class BistroBenchmarkComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+        , public AZ::Data::AssetBus::MultiHandler
+    {
+    public:
+        AZ_COMPONENT(BistroBenchmarkComponent, "{2AFFAA6B-1795-4635-AFAD-C2A98163832F}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        BistroBenchmarkComponent() = default;
+        ~BistroBenchmarkComponent() override = default;
+
+        //AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        // AZ::Data::AssetBus::Handler
+        void OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset);
+
+        void BenchmarkLoadStart();
+        void FinalizeLoadBenchmarkData();
+        void BenchmarkLoadEnd();
+
+        void BenchmarkRunStart();
+        void CollectRunBenchmarkData(float deltaTime, AZ::ScriptTimePoint timePoint);
+        void FinalizeRunBenchmarkData();
+        void BenchmarkRunEnd();
+
+        void DisplayLoadingDialog();
+        void DisplayResults();
+
+        struct LoadBenchmarkData
+        {
+            AZ_TYPE_INFO(LoadBenchmarkData, "{8677FEAB-EBA6-468A-97CB-AAF1B16F5FE2}");
+
+            static void Reflect(AZ::ReflectContext* context);
+
+            struct FileLoadedData
+            {
+                AZ_TYPE_INFO(FileLoadedData, "{F4E5728D-C526-4C28-83BA-D4B07DC894E8}");
+
+                static void Reflect(AZ::ReflectContext* context);
+
+                AZStd::string m_relativePath;
+                AZ::u64 m_bytesLoaded = 0;
+            };
+
+            AZStd::string m_name = "";
+            double m_timeInSeconds = 0.0;
+            double m_totalMBLoaded = 0.0;
+            double m_mbPerSec = 0.0;
+            AZ::u64 m_numFilesLoaded = 0;
+            AZStd::vector<FileLoadedData> m_filesLoaded;
+        };
+
+        AZStd::vector<AZ::Data::AssetId> m_trackedAssets;
+
+        struct RunBenchmarkData
+        {
+            AZ_TYPE_INFO(RunBenchmarkData, "{45FFA85B-1224-4558-B833-3D8A4404041C}");
+
+            static void Reflect(AZ::ReflectContext* context);
+
+            AZStd::string m_name;
+            AZStd::vector<float> m_frameTimes; // not serialized
+            AZStd::vector<float> m_frameRates; // not serialized
+            AZ::u64 m_frameCount;
+            double m_timeInSeconds = 0.0;
+            double m_timeToFirstFrame = 0.0;
+            double m_averageFrameTime = 0.0;
+            float m_50pFramesUnder = 0.0f;
+            float m_90pFramesUnder = 0.0f;
+            float m_minFrameTime = FLT_MAX;
+            float m_maxFrameTime = FLT_MIN;
+
+            float m_averageFrameRate = 0.0;
+            float m_minFrameRate = FLT_MAX;
+            float m_maxFrameRate = FLT_MIN;
+        };
+
+        double m_currentTimePointInSeconds = 0.0;
+
+        double m_benchmarkStartTimePoint = 0.0;
+
+        double m_timeToFirstFrame = 0.0;
+
+        LoadBenchmarkData m_currentLoadBenchmarkData;
+        RunBenchmarkData m_currentRunBenchmarkData;
+
+        struct CameraPathPoint
+        {
+            AZ::u64 m_framePoint;
+            AZ::Vector3 m_position;
+            AZ::Vector3 m_target;
+            AZ::Vector3 m_up;
+        };
+        using CameraPath = AZStd::vector<CameraPathPoint>;
+
+        const CameraPath m_exteriorPath = {
+            {0,     AZ::Vector3(13.924f, 42.245f, 2.761f), AZ::Vector3(13.661f, 41.280f, 2.748f), AZ::Vector3(0.006f, 0.0636f, 0.998f)},
+            {500,   AZ::Vector3(11.651f, 35.829f, 2.761f), AZ::Vector3(11.389f, 34.864f, 2.748f), AZ::Vector3(0.006f, 0.0636f, 0.998f)},
+            {1000,  AZ::Vector3(6.611f, 25.962f, 2.737f),  AZ::Vector3(6.037f, 25.143f, 2.733f),  AZ::Vector3(0.045f, 0.056f, 0.997f)},
+            {1500,  AZ::Vector3(2.655f, 21.641f, 2.737f),  AZ::Vector3(2.081f, 20.822f, 2.733f),  AZ::Vector3(0.045f, 0.056f, 0.997f)},
+            {2000,  AZ::Vector3(-1.235f, 14.989f, 1.450f), AZ::Vector3(-0.897f, 14.054f, 1.346f), AZ::Vector3(0.017f, -0.024f, 0.999f)},
+            {2500,  AZ::Vector3(-4.379f, 10.890f, 1.589f), AZ::Vector3(-3.842f, 10.058f, 1.454f), AZ::Vector3(0.025f, -0.055f, 0.998f)},
+            {3000,  AZ::Vector3(-7.152f, 4.652f, 1.369f),  AZ::Vector3(-6.159f, 4.749f, 1.317f),  AZ::Vector3(-0.024, -0.010f, 0.999f)},
+            {3500,  AZ::Vector3(-3.468f, -1.993f, 3.570f),   AZ::Vector3(-2.898f, -1.187f, 3.406f),   AZ::Vector3(0.052f, 0.071f, 0.996f)},
+            {4000,  AZ::Vector3(1.469f, -4.083f, 3.076f),    AZ::Vector3(1.924f, -3.197f, 2.982f),    AZ::Vector3(0.009f, 0.016f, 0.999f)},
+            {4500,  AZ::Vector3(7.631f, -5.290f, 3.476f),    AZ::Vector3(8.571f, -5.631f, 3.501f),    AZ::Vector3(-0.096f, 0.032f, 0.995f)},
+            {5000,  AZ::Vector3(28.078f, -13.430f, 3.165f),  AZ::Vector3(29.019f, -13.766f, 3.145f),  AZ::Vector3(-0.055f, 0.015f, 0.998f)},
+            {5500,  AZ::Vector3(37.032f, -21.699f, 3.082f),  AZ::Vector3(38.013f, -21.507f, 3.068f),  AZ::Vector3(-0.067f, 0.015f, 0.998f)},
+            {6000,  AZ::Vector3(40.579f, -27.793f, 2.948f),  AZ::Vector3(41.019f, -26.895f, 2.954f),  AZ::Vector3(-0.054f, -0.065f, 0.996f)},
+            {6500,  AZ::Vector3(52.912f, -27.212f, 3.632f),  AZ::Vector3(52.160f, -26.554f, 3.593f),  AZ::Vector3(0.026f, -0.027f, 0.999f)},
+            {7000,  AZ::Vector3(51.839f, -13.224f, 4.111f),  AZ::Vector3(51.241f, -14.023f, 4.044f),  AZ::Vector3(0.013f, 0.002f, 0.999f)},
+            {7500,  AZ::Vector3(36.274f, -14.114f, 3.613f),  AZ::Vector3(35.314f, -13.836f, 3.566f),  AZ::Vector3(0.026f, -0.016f, 0.999f)},
+            {8000,  AZ::Vector3(19.405f, -9.554f, 3.259f),   AZ::Vector3(18.459f, -9.229f, 3.248f),   AZ::Vector3(0.059f, -0.025f, 0.997f)},
+            {8500,  AZ::Vector3(7.768f, -4.673f, 2.460f),    AZ::Vector3(7.320f, -3.786f, 2.345f),    AZ::Vector3(-0.047f, 0.019f, 0.998f)},
+            {9000,  AZ::Vector3(5.322f, 0.631f, 1.789f),   AZ::Vector3(4.787f, 1.470f, 1.684f),   AZ::Vector3(-0.043f, 0.006f, 0.999f)},
+            {9500,  AZ::Vector3(-0.990f, 2.766f, 1.955f),  AZ::Vector3(-1.060f, 3.759f, 1.867f),  AZ::Vector3(-0.029f, 0.009f, 0.999f)},
+            {10000, AZ::Vector3(-6.400f, 6.291f, 1.320f),  AZ::Vector3(-5.431f, 6.222f, 1.086f),  AZ::Vector3(0.160f, 0.007f, 0.987f)},
+        };
+
+        bool m_firstResultsDisplay = true;
+
+        bool m_startBenchmarkCapture = true;
+        bool m_endBenchmarkCapture = true;
+
+        AZ::u64 m_frameCount = 0;
+
+        CameraPathPoint m_currentCameraPoint;
+        CameraPathPoint m_lastCameraPoint;
+        
+
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_bistroExteriorAsset;
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_bistroInteriorAsset;
+
+        using MeshHandle = AZ::Render::MeshFeatureProcessorInterface::MeshHandle;
+        MeshHandle m_bistroExteriorMeshHandle;
+        MeshHandle m_bistroInteriorMeshHandle;
+        Utils::DefaultIBL m_defaultIbl;
+
+        bool m_bistroExteriorLoaded = false;
+        bool m_bistroInteriorLoaded = false;
+
+        AZ::Component* m_cameraControlComponent = nullptr;
+
+        AZStd::vector<uint64_t> m_framesToCapture;
+        AZStd::string m_screenshotFolder;
+
+        // Lights
+        AZ::Render::DirectionalLightFeatureProcessorInterface* m_directionalLightFeatureProcessor = nullptr;
+        AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle m_directionalLightHandle;
+
+        AZ::Render::SkyBoxFeatureProcessorInterface* m_skyboxFeatureProcessor = nullptr;
+    };
+} // namespace AtomSampleViewer

+ 472 - 0
Gem/Code/Source/BloomExampleComponent.cpp

@@ -0,0 +1,472 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <BloomExampleComponent.h>
+
+#include <AzCore/Component/Entity.h>
+#include <AzCore/IO/Path/Path.h>
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Entity/EntityContextBus.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <EntityUtilityFunctions.h>
+
+#include <Atom/Feature/Utils/FrameCaptureBus.h>
+
+#include <Atom/RHI/DrawPacketBuilder.h>
+
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Shader/Shader.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+
+    void BloomExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<BloomExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    BloomExampleComponent::BloomExampleComponent()
+        : m_imageBrowser("@user@/BloomExampleComponent/image_browser.xml")
+    {
+    }
+
+    void BloomExampleComponent::Activate()
+    {
+        m_dynamicDraw = RPI::GetDynamicDraw();
+
+        RPI::Scene* scene = RPI::RPISystemInterface::Get()->GetDefaultScene().get();
+
+        m_postProcessFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PostProcessFeatureProcessorInterface>();
+
+        CreateBloomEntity();
+
+        m_imguiSidebar.Activate();
+
+        AZ::TickBus::Handler::BusConnect();
+
+        PrepareRenderData();
+
+        m_imageBrowser.SetFilter([this](const AZ::Data::AssetInfo& assetInfo)
+        {
+            if (!AzFramework::StringFunc::Path::IsExtension(assetInfo.m_relativePath.c_str(), "streamingimage"))
+            {
+                return false;
+            }
+            AZStd::string assetPath(assetInfo.m_relativePath);
+            if (!AzFramework::StringFunc::Path::Normalize(assetPath))
+            {
+                return false;
+            }
+
+            if (!Utils::IsFileUnderFolder(assetPath, InputImageFolder))
+            {
+                return false;
+            }
+
+            return true;
+        });
+        m_imageBrowser.Activate();
+
+        // Load a default image
+        QueueAssetPathForLoad("textures/tonemapping/hdr_test_pattern.exr.streamingimage");
+
+        const char* engineRoot = nullptr;
+        AZStd::string screenshotFolder;
+        AzFramework::ApplicationRequests::Bus::BroadcastResult(engineRoot, &AzFramework::ApplicationRequests::GetEngineRoot);
+        if (engineRoot)
+        {
+            AzFramework::StringFunc::Path::Join(engineRoot, "Screenshots", screenshotFolder, true, false);
+        }
+
+        Data::Asset<RPI::AnyAsset> displayMapperAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::AnyAsset>("passes/DisplayMapperConfiguration.azasset", RPI::AssetUtils::TraceLevel::Error);
+        const Render::DisplayMapperConfigurationDescriptor* displayMapperConfigurationDescriptor = RPI::GetDataFromAnyAsset<Render::DisplayMapperConfigurationDescriptor>(displayMapperAsset);
+        if (displayMapperConfigurationDescriptor == nullptr)
+        {
+            AZ_Error("DisplayMapperPass", false, "Failed to load display mapper configuration file.");
+            return;
+        }
+        m_displayMapperConfiguration = *displayMapperConfigurationDescriptor;
+    }
+
+    void BloomExampleComponent::Deactivate()
+    {
+        AZ::TickBus::Handler::BusDisconnect();
+
+        AZ::EntityBus::MultiHandler::BusDisconnect();
+
+        if (m_bloomEntity)
+        {
+            DestroyEntity(m_bloomEntity, GetEntityContextId());
+            m_postProcessFeatureProcessor = nullptr;
+        }
+
+        m_imguiSidebar.Deactivate();
+        m_imageBrowser.Deactivate();
+    }
+
+    void BloomExampleComponent::OnEntityDestruction(const AZ::EntityId& entityId)
+    {
+        AZ::EntityBus::MultiHandler::BusDisconnect(entityId);
+
+        if (m_bloomEntity && m_bloomEntity->GetId() == entityId)
+        {
+            m_postProcessFeatureProcessor->RemoveSettingsInterface(m_bloomEntity->GetId());
+            m_bloomEntity = nullptr;
+        }
+        else
+        {
+            AZ_Assert(false, "unexpected entity destruction is signaled.");
+        }
+    }
+
+    void BloomExampleComponent::OnTick([[maybe_unused]] float deltaTime, AZ::ScriptTimePoint)
+    {
+        if (m_drawImage.m_image && !m_drawImage.m_wasStreamed)
+        {
+            m_drawImage.m_wasStreamed = m_drawImage.m_image->GetResidentMipLevel() == 0;
+
+            m_drawImage.m_srg->Compile();
+        }
+
+        DrawSidebar();
+        DrawImage(&m_drawImage);
+    }
+
+    void BloomExampleComponent::DrawSidebar()
+    {
+        using namespace AZ::Render;
+
+        const char* items[] = { "White", "Red", "Green", "Blue" };
+        static int item_current0 = 0;
+        static int item_current1 = 0;
+        static int item_current2 = 0;
+        static int item_current3 = 0;
+        static int item_current4 = 0;
+
+        auto ColorPicker = [](int index)->Vector3 {
+            switch (index)
+            {
+            case 0: return Vector3(1.0, 1.0, 1.0); break;
+            case 1: return Vector3(1.0, 0.5, 0.5); break;
+            case 2: return Vector3(0.5, 1.0, 0.5); break;
+            case 3: return Vector3(0.5, 0.5, 1.0); break;
+            default: return Vector3(1.0, 1.0, 1.0);
+            }
+        };
+
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        ImGui::Spacing();
+
+        bool enabled = m_bloomSettings->GetEnabled();
+        if (ImGui::Checkbox("Enable", &enabled) || m_isInitParameters)
+        {
+            m_bloomSettings->SetEnabled(enabled);
+            m_bloomSettings->OnConfigChanged();
+        }
+
+        ImGui::Spacing();
+
+        float threshold = m_bloomSettings->GetThreshold();
+        if (ImGui::SliderFloat("Threshold", &threshold, 0.0f, 20.0f, "%0.1f") || !m_isInitParameters)
+        {
+            m_bloomSettings->SetThreshold(threshold);
+            m_bloomSettings->OnConfigChanged();
+        }
+
+        float knee = m_bloomSettings->GetKnee();
+        if (ImGui::SliderFloat("Knee", &knee, 0.0f, 1.0f) || m_isInitParameters)
+        {
+            m_bloomSettings->SetKnee(knee);
+            m_bloomSettings->OnConfigChanged();
+        }
+
+        ImGui::Spacing();
+
+        float sizeScale = m_bloomSettings->GetKernelSizeScale();
+        if (ImGui::SliderFloat("KernelSizeScale", &sizeScale, 0.0f, 2.0f) || m_isInitParameters)
+        {
+            m_bloomSettings->SetKernelSizeScale(sizeScale);
+            m_bloomSettings->OnConfigChanged();
+        }
+
+        if (ImGui::CollapsingHeader("KernelSize", ImGuiTreeNodeFlags_DefaultOpen))
+        {
+            ImGui::Indent();
+
+            float kernelSize0 = m_bloomSettings->GetKernelSizeStage0();
+            if (ImGui::SliderFloat("Size0", &kernelSize0, 0.0f, 1.0f) || m_isInitParameters)
+            {
+                m_bloomSettings->SetKernelSizeStage0(kernelSize0);
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            float kernelSize1 = m_bloomSettings->GetKernelSizeStage1();
+            if (ImGui::SliderFloat("Size1", &kernelSize1, 0.0f, 1.0f) || m_isInitParameters)
+            {
+                m_bloomSettings->SetKernelSizeStage1(kernelSize1);
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            float kernelSize2 = m_bloomSettings->GetKernelSizeStage2();
+            if (ImGui::SliderFloat("Size2", &kernelSize2, 0.0f, 1.0f) || m_isInitParameters)
+            {
+                m_bloomSettings->SetKernelSizeStage2(kernelSize2);
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            float kernelSize3 = m_bloomSettings->GetKernelSizeStage3();
+            if (ImGui::SliderFloat("Size3", &kernelSize3, 0.0f, 1.0f) || m_isInitParameters)
+            {
+                m_bloomSettings->SetKernelSizeStage3(kernelSize3);
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            float kernelSize4 = m_bloomSettings->GetKernelSizeStage4();
+            if (ImGui::SliderFloat("Size4", &kernelSize4, 0.0f, 1.0f) || m_isInitParameters)
+            {
+                m_bloomSettings->SetKernelSizeStage4(kernelSize4);
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            ImGui::Unindent();
+        }
+
+        ImGui::Spacing();
+
+        float intensity = m_bloomSettings->GetIntensity();
+        if (ImGui::SliderFloat("Intensity", &intensity, 0.0f, 1.0f) || m_isInitParameters)
+        {
+            m_bloomSettings->SetIntensity(intensity);
+            m_bloomSettings->OnConfigChanged();
+        }
+
+        if (ImGui::CollapsingHeader("Tint", ImGuiTreeNodeFlags_DefaultOpen))
+        {
+            ImGui::Indent();
+
+            if (ImGui::ListBox("Tint0", &item_current0, items, IM_ARRAYSIZE(items), 4) || m_isInitParameters)
+            {
+                m_bloomSettings->SetTintStage0(ColorPicker(item_current0));
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            if (ImGui::ListBox("Tint1", &item_current1, items, IM_ARRAYSIZE(items), 4) || m_isInitParameters)
+            {
+                m_bloomSettings->SetTintStage1(ColorPicker(item_current1));
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            if (ImGui::ListBox("Tint2", &item_current2, items, IM_ARRAYSIZE(items), 4) || m_isInitParameters)
+            {
+                m_bloomSettings->SetTintStage2(ColorPicker(item_current2));
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            if (ImGui::ListBox("Tint3", &item_current3, items, IM_ARRAYSIZE(items), 4) || m_isInitParameters)
+            {
+                m_bloomSettings->SetTintStage3(ColorPicker(item_current3));
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            if (ImGui::ListBox("Tint4", &item_current4, items, IM_ARRAYSIZE(items), 4) || m_isInitParameters)
+            {
+                m_bloomSettings->SetTintStage4(ColorPicker(item_current4));
+                m_bloomSettings->OnConfigChanged();
+            }
+
+            ImGui::Unindent();
+        }
+
+        ImGui::Spacing();
+
+        bool bicubicEnabled = m_bloomSettings->GetBicubicEnabled();
+        if (ImGui::Checkbox("Bicubic upsampling", &bicubicEnabled) || m_isInitParameters)
+        {
+            m_bloomSettings->SetBicubicEnabled(bicubicEnabled);
+            m_bloomSettings->OnConfigChanged();
+        }
+
+        m_imguiSidebar.End();
+    }
+
+    void BloomExampleComponent::PrepareRenderData()
+    {
+        const auto CreatePipeline = [](const char* shaderFilepath,
+            const char* srgFilepath,
+            Data::Asset<AZ::RPI::ShaderResourceGroupAsset>& srgAsset,
+            RHI::ConstPtr<RHI::PipelineState>& pipelineState,
+            RHI::DrawListTag& drawListTag)
+        {
+            // Since the shader is using SV_VertexID and SV_InstanceID as VS input, we won't need to have vertex buffer.
+            // Also, the index buffer is not needed with DrawLinear.
+            RHI::PipelineStateDescriptorForDraw pipelineStateDescriptor;
+
+            Data::Asset<RPI::ShaderAsset> shaderAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::ShaderAsset>(shaderFilepath, RPI::AssetUtils::TraceLevel::Error);
+            Data::Instance<RPI::Shader> shader = RPI::Shader::FindOrCreate(shaderAsset);
+
+            if (!shader)
+            {
+                AZ_Error("Render", false, "Failed to find or create shader instance from shader asset with path %s", shaderFilepath);
+                return;
+            }
+
+            const RPI::ShaderVariant& shaderVariant = shader->GetVariant(RPI::ShaderAsset::RootShaderVariantStableId);
+            shaderVariant.ConfigurePipelineState(pipelineStateDescriptor);
+            drawListTag = shader->GetDrawListTag();
+
+            RPI::Scene* scene = RPI::RPISystemInterface::Get()->GetDefaultScene().get();
+            scene->ConfigurePipelineState(shader->GetDrawListTag(), pipelineStateDescriptor);
+
+            pipelineStateDescriptor.m_inputStreamLayout.SetTopology(AZ::RHI::PrimitiveTopology::TriangleStrip);
+            pipelineStateDescriptor.m_inputStreamLayout.Finalize();
+
+            pipelineState = shader->AcquirePipelineState(pipelineStateDescriptor);
+            if (!pipelineState)
+            {
+                AZ_Error("Render", false, "Failed to acquire default pipeline state for shader %s", shaderFilepath);
+            }
+
+            // Load shader resource group asset
+            srgAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::ShaderResourceGroupAsset>(srgFilepath, RPI::AssetUtils::TraceLevel::Error);
+        };
+
+        // Create the example's main pipeline object
+        {
+            CreatePipeline("Shaders/tonemappingexample/renderimage.azshader", "Shaders/tonemappingexample/renderimage_renderimagesrg.azsrg", m_srgAsset, m_pipelineState, m_drawListTag);
+
+            // Set the input indices
+            m_imageInputIndex = m_srgAsset->GetLayout()->FindShaderInputImageIndex(Name("m_texture"));
+            m_positionInputIndex = m_srgAsset->GetLayout()->FindShaderInputConstantIndex(Name("m_position"));
+            m_sizeInputIndex = m_srgAsset->GetLayout()->FindShaderInputConstantIndex(Name("m_size"));
+            m_colorSpaceIndex = m_srgAsset->GetLayout()->FindShaderInputConstantIndex(Name("m_colorSpace"));
+        }
+
+        m_drawImage.m_srg = RPI::ShaderResourceGroup::Create(m_srgAsset);
+        m_drawImage.m_wasStreamed = false;
+
+        // Set the image to occupy the full screen.
+        // The window's left bottom is (-1, -1). The window size is (2, 2)
+        AZStd::array<float, 2> position, size;
+        position[0] = -1.f;
+        position[1] = -1.f;
+        size[0] = 2.0f;
+        size[1] = 2.0f;
+
+        m_drawImage.m_srg->SetConstant(m_positionInputIndex, position);
+        m_drawImage.m_srg->SetConstant(m_sizeInputIndex, size);
+        m_drawImage.m_srg->SetConstant<int>(m_colorSpaceIndex, static_cast<int>(m_inputColorSpace));
+    }
+
+    void BloomExampleComponent::DrawImage(const ImageToDraw* imageInfo)
+    {
+        // Build draw packet
+        RHI::DrawPacketBuilder drawPacketBuilder;
+        drawPacketBuilder.Begin(nullptr);
+        RHI::DrawLinear drawLinear;
+        drawLinear.m_vertexCount = 4;
+        drawPacketBuilder.SetDrawArguments(drawLinear);
+
+        RHI::DrawPacketBuilder::DrawRequest drawRequest;
+        drawRequest.m_listTag = m_drawListTag;
+        drawRequest.m_pipelineState = m_pipelineState.get();
+        drawRequest.m_sortKey = 0;
+        drawRequest.m_uniqueShaderResourceGroup = imageInfo->m_srg->GetRHIShaderResourceGroup();
+        drawPacketBuilder.AddDrawItem(drawRequest);
+
+        // Submit draw packet
+        AZStd::unique_ptr<const RHI::DrawPacket> drawPacket(drawPacketBuilder.End());
+        m_dynamicDraw->AddDrawPacket(RPI::RPISystemInterface::Get()->GetDefaultScene().get(), AZStd::move(drawPacket));
+    }
+
+    RPI::ColorSpaceId BloomExampleComponent::GetColorSpaceIdForIndex(uint8_t colorSpaceIndex) const
+    {
+        const AZStd::vector<AZ::RPI::ColorSpaceId> colorSpaces =
+        {
+            RPI::ColorSpaceId::SRGB,
+            RPI::ColorSpaceId::LinearSRGB,
+            RPI::ColorSpaceId::ACEScg,
+            RPI::ColorSpaceId::ACES2065
+        };
+        if (colorSpaceIndex >= colorSpaces.size())
+        {
+            AZ_Assert(false, "Invalid colorSpaceIndex");
+            return RPI::ColorSpaceId::SRGB;
+        }
+        return colorSpaces[colorSpaceIndex];
+    }
+
+    void BloomExampleComponent::CreateBloomEntity()
+    {
+        m_bloomEntity = CreateEntity("Bloom", GetEntityContextId());
+
+        // Bloom
+        auto* bloomSettings = m_postProcessFeatureProcessor->GetOrCreateSettingsInterface(m_bloomEntity->GetId());
+        m_bloomSettings = bloomSettings->GetOrCreateBloomSettingsInterface();
+        m_bloomSettings->SetEnabled(false);
+
+        m_bloomEntity->Activate();
+        AZ::EntityBus::MultiHandler::BusConnect(m_bloomEntity->GetId());
+    }
+
+    void BloomExampleComponent::QueueAssetPathForLoad(const AZStd::string& filePath)
+    {
+        AZ::Data::AssetId imageAssetId;
+        AZ::Data::AssetCatalogRequestBus::BroadcastResult(
+            imageAssetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, filePath.c_str(),
+            azrtti_typeid <AZ::RPI::StreamingImageAsset>(), false);
+        AZ_Assert(imageAssetId.IsValid(), "Unable to load file %s", filePath.c_str());
+        QueueAssetIdForLoad(imageAssetId);
+    }
+
+    void BloomExampleComponent::QueueAssetIdForLoad(const AZ::Data::AssetId& imageAssetId)
+    {
+        if (imageAssetId.IsValid())
+        {
+            Data::Asset<AZ::RPI::StreamingImageAsset> imageAsset;
+            if (!imageAsset.Create(imageAssetId))
+            {
+                auto assetId = imageAssetId.ToString<AZStd::string>();
+                AZ_Assert(false, "Unable to create image asset for asset ID %s", assetId.c_str());
+                return;
+            }
+
+            auto image = AZ::RPI::StreamingImage::FindOrCreate(imageAsset);
+            if (image == nullptr)
+            {
+                auto imageAssetName = imageAssetId.ToString<AZStd::string>();
+                AZ_Assert(false, "Failed to find or create an image instance from image asset %s", imageAssetName.c_str());
+                return;
+            }
+
+            m_drawImage.m_assetId = imageAssetId;
+            m_drawImage.m_image = image;
+            m_drawImage.m_wasStreamed = false;
+            m_drawImage.m_srg->SetImage(m_imageInputIndex, m_drawImage.m_image);
+        }
+    }
+} // namespace AtomSampleViewer

+ 130 - 0
Gem/Code/Source/BloomExampleComponent.h

@@ -0,0 +1,130 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <Atom/Feature/DisplayMapper/DisplayMapperConfigurationDescriptor.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessorInterface.h>
+#include <Atom/Feature/PostProcess/PostProcessFeatureProcessorInterface.h>
+#include <Atom/RPI.Public/ColorManagement/TransformColor.h>
+#include <Atom/RPI.Public/DynamicDraw/DynamicDrawInterface.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/Component/EntityBus.h>
+
+#include <AzFramework/Entity/EntityContextBus.h>
+
+#include <Utils/Utils.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/ImGuiAssetBrowser.h>
+#include <Utils/ImGuiSaveFilePath.h>
+
+namespace AtomSampleViewer
+{
+    //! This component reuses the scene of tonemapping example to demonstrate the bloom feature
+    class BloomExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(BloomExampleComponent, "{F1957895-D74D-4A82-BF0C-055B57AB698A}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        BloomExampleComponent();
+        ~BloomExampleComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+        // AZ::EntityBus::MultiHandler...
+        void OnEntityDestruction(const AZ::EntityId& entityId) override;
+
+        // GUI
+        void DrawSidebar();
+        
+        // Image loading and capture
+        AZ::RPI::ColorSpaceId GetColorSpaceIdForIndex(uint8_t colorSpaceIndex) const;
+        
+        struct ImageToDraw
+        {
+            AZ::Data::AssetId m_assetId;
+            AZ::Data::Instance<AZ::RPI::StreamingImage> m_image;
+            AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> m_srg;
+            bool m_wasStreamed = false;
+        };
+
+        // Submit draw package to draw image
+        void DrawImage(const ImageToDraw* imageInfo);
+
+        // Creates resources, resource views, pipeline state, etc. for rendering
+        void PrepareRenderData();
+        void QueueAssetPathForLoad(const AZStd::string& filePath);
+        void QueueAssetIdForLoad(const AZ::Data::AssetId& imageAssetId);
+
+        // Create bloom entity for rendering
+        void CreateBloomEntity();
+
+        // non-owning entity
+        AzFramework::EntityContextId m_entityContextId;
+
+        // owning entity
+        AZ::Entity* m_bloomEntity = nullptr;
+
+        // Init flag
+        bool m_isInitParameters = false;
+
+        // GUI
+        ImGuiSidebar m_imguiSidebar;
+
+        // Cache the DynamicDraw Interface
+        AZ::RPI::DynamicDrawInterface* m_dynamicDraw = nullptr;
+
+        // render related data
+        AZ::RHI::ConstPtr<AZ::RHI::PipelineState> m_pipelineState;
+        AZ::RHI::DrawListTag m_drawListTag;
+        AZ::Data::Asset<AZ::RPI::ShaderResourceGroupAsset> m_srgAsset;
+
+        // shader input indices
+        AZ::RHI::ShaderInputImageIndex m_imageInputIndex;
+        AZ::RHI::ShaderInputConstantIndex m_positionInputIndex;
+        AZ::RHI::ShaderInputConstantIndex m_sizeInputIndex;
+        AZ::RHI::ShaderInputConstantIndex m_colorSpaceIndex;
+
+        // post processing feature processor
+        AZ::Render::PostProcessFeatureProcessorInterface* m_postProcessFeatureProcessor = nullptr;
+        AZ::Render::BloomSettingsInterface* m_bloomSettings = nullptr;
+
+        // Image to display
+        ImageToDraw m_drawImage;
+
+        AZ::RPI::ColorSpaceId m_inputColorSpace = AZ::RPI::ColorSpaceId::ACEScg;
+        static const char* s_colorSpaceLabels[];
+        int m_inputColorSpaceIndex = 0;
+
+        const AZStd::string InputImageFolder = "textures\\bloom\\";
+
+        AZ::Render::DisplayMapperConfigurationDescriptor m_displayMapperConfiguration;
+
+        AZStd::vector<AZStd::string> m_capturePassHierarchy;
+
+        ImGuiAssetBrowser m_imageBrowser;
+    };
+} // namespace AtomSampleViewer

+ 159 - 0
Gem/Code/Source/CheckerboardExampleComponent.cpp

@@ -0,0 +1,159 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <CheckerboardExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/RPI.Public/View.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+
+#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    void CheckerboardExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class < CheckerboardExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    CheckerboardExampleComponent::CheckerboardExampleComponent()
+    {
+    }
+
+    void CheckerboardExampleComponent::Activate()
+    {
+        AZ::RPI::AssetUtils::TraceLevel traceLevel = AZ::RPI::AssetUtils::TraceLevel::Assert;
+        auto meshAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>("objects/shaderball_simple.azmodel", traceLevel);
+        auto materialAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::MaterialAsset>(DefaultPbrMaterialPath, traceLevel);
+
+        AZ::Render::MaterialAssignmentMap materials;
+        AZ::Render::MaterialAssignment& defaultMaterial = materials[AZ::Render::DefaultMaterialAssignmentId];
+        defaultMaterial.m_materialAsset = materialAsset;
+        defaultMaterial.m_materialInstance = AZ::RPI::Material::FindOrCreate(defaultMaterial.m_materialAsset);
+
+        AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        m_meshFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::MeshFeatureProcessorInterface>();
+        m_meshHandle = m_meshFeatureProcessor->AcquireMesh(meshAsset, materials);
+        m_meshFeatureProcessor->SetTransform(m_meshHandle, AZ::Transform::CreateIdentity());
+        
+        AZ::Debug::CameraControllerRequestBus::Event(m_cameraEntityId, &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::ArcBallControllerComponent>());
+
+        // Add an Image based light.
+        m_defaultIbl.Init(scene.get());
+
+
+        AZ::TickBus::Handler::BusConnect();
+
+        // connect to the bus before creating new pipeline
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusConnect();
+
+        ActivateCheckerboardPipeline();
+
+        // Create an ImGuiActiveContextScope to ensure the ImGui context on the new pipeline's ImGui pass is activated.
+        m_imguiScope = AZ::Render::ImGuiActiveContextScope::FromPass(AZ::RPI::PassHierarchyFilter({ "CheckerboardPipeline", "ImGuiPass" }));
+        m_imguiSidebar.Activate();
+    }
+
+    void CheckerboardExampleComponent::Deactivate()
+    {
+        m_imguiSidebar.Deactivate();
+        m_imguiScope = {}; // restores previous ImGui context.
+
+        DeactivateCheckerboardPipeline();
+
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusDisconnect();
+
+        AZ::TickBus::Handler::BusDisconnect();
+
+        m_defaultIbl.Reset();
+
+        m_meshFeatureProcessor->ReleaseMesh(m_meshHandle);
+        m_meshFeatureProcessor = nullptr;
+    }
+    
+    bool CheckerboardExampleComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
+    {
+        auto config = azrtti_cast<const SampleComponentConfig*>(baseConfig);
+        AZ_Assert(config && config->IsValid(), "SampleComponentConfig required for sample component configuration.");
+        m_cameraEntityId = config->m_cameraEntityId;
+        m_entityContextId = config->m_entityContextId;
+        return true;
+    }
+
+    void CheckerboardExampleComponent::DefaultWindowCreated()
+    {
+        AZ::Render::Bootstrap::DefaultWindowBus::BroadcastResult(m_windowContext,
+            &AZ::Render::Bootstrap::DefaultWindowBus::Events::GetDefaultWindowContext);
+    }
+
+    void CheckerboardExampleComponent::ActivateCheckerboardPipeline()
+    {        
+        // save original render pipeline first and remove it from the scene
+        AZ::RPI::ScenePtr defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        m_originalPipeline = defaultScene->GetDefaultRenderPipeline();
+        defaultScene->RemoveRenderPipeline(m_originalPipeline->GetId());
+
+        // add the checker board pipeline
+        AZ::RPI::RenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.m_mainViewTagName = "MainCamera";
+        pipelineDesc.m_name = "Checkerboard";
+        pipelineDesc.m_rootPassTemplate = "CheckerboardPipeline";
+        m_cbPipeline = AZ::RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext);
+        defaultScene->AddRenderPipeline(m_cbPipeline);
+        m_cbPipeline->SetDefaultView(m_originalPipeline->GetDefaultView());
+    }
+
+    void CheckerboardExampleComponent::DeactivateCheckerboardPipeline()
+    {
+        // remove cb pipeline before adding original pipeline.
+        AZ::RPI::ScenePtr defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        defaultScene->RemoveRenderPipeline(m_cbPipeline->GetId());
+
+        defaultScene->AddRenderPipeline(m_originalPipeline);
+
+        m_cbPipeline = nullptr;
+    }
+
+    void CheckerboardExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        DrawSidebar();
+    }
+
+    void CheckerboardExampleComponent::DrawSidebar()
+    {
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        ImGui::Text("Checkerboard debug render");
+
+        m_imguiSidebar.End();
+    }
+}

+ 84 - 0
Gem/Code/Source/CheckerboardExampleComponent.h

@@ -0,0 +1,84 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <Atom/Bootstrap/DefaultWindowBus.h>
+#include <Atom/Feature/ImGui/ImGuiUtils.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessorInterface.h>
+
+#include <AzCore/Component/Component.h>
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <AzFramework/Entity/EntityContextBus.h>
+
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/Utils.h>
+
+namespace AtomSampleViewer
+{
+    // This component renders a model with pbr material using checkerboard render pipeline.
+    class CheckerboardExampleComponent final
+        : public AZ::Component
+        , public AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(CheckerboardExampleComponent, "{0E5B3D5F-5BD2-41BF-BB1E-425AF976DFC9}");
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        CheckerboardExampleComponent();
+        ~CheckerboardExampleComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        // AZ::Component overrides...
+        bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override;
+
+        // DefaultWindowNotificationBus::Handler overrides...
+        void DefaultWindowCreated() override;
+        
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        void ActivateCheckerboardPipeline();
+        void DeactivateCheckerboardPipeline();
+
+        // draw debug menu
+        void DrawSidebar();
+                        
+        // shader ball model
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+
+        // Not owned by this sample, we look this up based on the config from the
+        // SampleComponentManager
+        AZ::EntityId m_cameraEntityId;
+        AzFramework::EntityContextId m_entityContextId;
+
+        AZ::Render::MeshFeatureProcessorInterface* m_meshFeatureProcessor = nullptr;
+        Utils::DefaultIBL m_defaultIbl;
+
+        // for checkerboard render pipeline
+        AZ::RPI::RenderPipelinePtr m_cbPipeline;
+        AZ::RPI::RenderPipelinePtr m_originalPipeline;
+        AZStd::shared_ptr<AZ::RPI::WindowContext> m_windowContext;
+
+        // debug menu
+        ImGuiSidebar m_imguiSidebar;
+        AZ::Render::ImGuiActiveContextScope m_imguiScope;
+    };
+} // namespace AtomSampleViewer

+ 258 - 0
Gem/Code/Source/CommonSampleComponentBase.cpp

@@ -0,0 +1,258 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <CommonSampleComponentBase.h>
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <Automation/ScriptableImGui.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+
+#include <RHI/BasicRHIComponent.h>
+#include <EntityUtilityFunctions.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+    using namespace RPI;
+
+    bool CommonSampleComponentBase::ReadInConfig(const ComponentConfig* baseConfig)
+    {
+        auto config = azrtti_cast<const SampleComponentConfig*>(baseConfig);
+        if (config && config->IsValid())
+        {
+            m_cameraEntityId = config->m_cameraEntityId;
+            m_entityContextId = config->m_entityContextId;
+            return true;
+        }
+        else
+        {
+            AZ_Assert(false, "SampleComponentConfig required for sample component configuration.");
+            return false;
+        }
+    }
+
+    AzFramework::EntityContextId CommonSampleComponentBase::GetEntityContextId() const
+    {
+        return m_entityContextId;
+    }
+
+    AZ::EntityId CommonSampleComponentBase::GetCameraEntityId() const
+    {
+        return m_cameraEntityId;
+    }
+
+    AZ::Render::MeshFeatureProcessorInterface* CommonSampleComponentBase::GetMeshFeatureProcessor() const
+    {
+        if (!m_meshFeatureProcessor)
+        {
+            m_meshFeatureProcessor = Scene::GetFeatureProcessorForEntityContextId<Render::MeshFeatureProcessorInterface>(m_entityContextId);
+            AZ_Assert(m_meshFeatureProcessor, "MeshFeatureProcessor not available.");
+        }
+
+        return m_meshFeatureProcessor;
+    }
+
+    void CommonSampleComponentBase::InitLightingPresets(bool loadDefaultLightingPresets)
+    {
+        AZ::Render::SkyBoxFeatureProcessorInterface* skyboxFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::SkyBoxFeatureProcessorInterface>(GetEntityContextId());
+        AZ_Assert(skyboxFeatureProcessor, "Unable to find SkyBoxFeatureProcessorInterface.");
+        skyboxFeatureProcessor->SetSkyboxMode(AZ::Render::SkyBoxMode::Cubemap);
+        skyboxFeatureProcessor->Enable(true);
+
+        // We don't necessarily need an entity but PostProcessFeatureProcessorInterface needs an ID to retrieve ExposureControlSettingsInterface.
+        AzFramework::EntityContextRequestBus::EventResult(m_postProcessEntity, m_entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, "postProcessEntity");
+        AZ_Assert(m_postProcessEntity != nullptr, "Failed to create post process entity.");
+        m_postProcessEntity->Activate();
+
+        if (loadDefaultLightingPresets)
+        {
+            AZStd::list<AZ::Data::AssetInfo> lightingAssetInfoList;
+            AZ::Data::AssetCatalogRequests::AssetEnumerationCB enumerateCB = [&lightingAssetInfoList]([[maybe_unused]] const AZ::Data::AssetId id, const AZ::Data::AssetInfo& info)
+            {
+                if (AzFramework::StringFunc::EndsWith(info.m_relativePath.c_str(), ".lightingpreset.azasset"))
+                {
+                    lightingAssetInfoList.push_front(info);
+                }
+            };
+
+            AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets, nullptr, enumerateCB, nullptr);
+
+            for (const auto& info : lightingAssetInfoList)
+            {
+                AppendLightingPresetsFromAsset(info.m_relativePath);
+            }
+        }
+
+        AZ::TransformNotificationBus::MultiHandler::BusConnect(m_cameraEntityId);
+        AZ::EntityBus::MultiHandler::BusConnect(m_postProcessEntity->GetId());
+    }
+
+    void CommonSampleComponentBase::ShutdownLightingPresets()
+    {
+        AZ::Render::SkyBoxFeatureProcessorInterface* skyboxFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::SkyBoxFeatureProcessorInterface>(m_entityContextId);
+        AZ::Render::DirectionalLightFeatureProcessorInterface* directionalLightFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::DirectionalLightFeatureProcessorInterface>(m_entityContextId);
+
+        for (AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle& handle : m_lightHandles)
+        {
+            directionalLightFeatureProcessor->ReleaseLight(handle);
+        }
+        m_lightHandles.clear();
+
+        ClearLightingPresets();
+
+        if (m_postProcessEntity)
+        {
+            DestroyEntity(m_postProcessEntity, GetEntityContextId());
+        }
+
+        skyboxFeatureProcessor->Enable(false);
+
+        AZ::TransformNotificationBus::MultiHandler::BusDisconnect();
+        AZ::EntityBus::MultiHandler::BusDisconnect();
+    }
+
+    void CommonSampleComponentBase::LoadLightingPresetsFromAsset(const AZStd::string& assetPath)
+    {
+        ClearLightingPresets();
+        AppendLightingPresetsFromAsset(assetPath);
+    }
+
+    void CommonSampleComponentBase::AppendLightingPresetsFromAsset(const AZStd::string& assetPath)
+    {
+        Data::Asset<AnyAsset> asset = AssetUtils::LoadAssetByProductPath<AnyAsset>(assetPath.c_str(), AssetUtils::TraceLevel::Error);
+        if (asset)
+        {
+            const AZ::Render::LightingPreset* preset = asset->GetDataAs<AZ::Render::LightingPreset>();
+            if (preset)
+            {
+                m_lightingPresets.push_back(*preset);
+            }
+            m_lightingPresetsDirty = true;
+            if (!m_lightingPresets.empty() && m_currentLightingPresetIndex == InvalidLightingPresetIndex)
+            {
+                m_currentLightingPresetIndex = 0;
+                OnLightingPresetSelected(m_lightingPresets[0]);
+            }
+        }
+        else
+        {
+            AZ_Error("Lighting Preset", false, "Lighting preset file failed to load from path: %s.", assetPath.c_str());
+        }
+    }
+
+    void CommonSampleComponentBase::ClearLightingPresets()
+    {
+        m_currentLightingPresetIndex = InvalidLightingPresetIndex;
+        m_lightingPresets.clear();
+        m_lightingPresetsDirty = true;
+    }
+
+    void CommonSampleComponentBase::ImGuiLightingPreset()
+    {
+        AZ_Assert((m_currentLightingPresetIndex == InvalidLightingPresetIndex) == m_lightingPresets.empty(),
+            "m_currentLightingPresetIndex should be invalid if and only if no lighting presets are loaded.");
+
+        if (m_currentLightingPresetIndex == InvalidLightingPresetIndex)
+        {
+            AZ_Warning("Lighting Preset", false, "Lighting presets must be loaded before calling ImGui.");
+            return;
+        }
+
+        AZStd::vector<const char*> items;
+        for (const auto& lightingPreset : m_lightingPresets)
+        {
+            items.push_back(lightingPreset.m_displayName.c_str());
+        }
+        if (ScriptableImGui::Combo("Lighting Preset##SampleBase", &m_currentLightingPresetIndex, items.begin(), aznumeric_cast<int>(items.size())))
+        {
+            OnLightingPresetSelected(m_lightingPresets[m_currentLightingPresetIndex]);
+        }
+    }
+
+    void CommonSampleComponentBase::OnLightingPresetSelected(const AZ::Render::LightingPreset& preset)
+    {
+        AZ::Render::SkyBoxFeatureProcessorInterface* skyboxFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::SkyBoxFeatureProcessorInterface>(m_entityContextId);
+        AZ::Render::PostProcessFeatureProcessorInterface* postProcessFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::PostProcessFeatureProcessorInterface>(m_entityContextId);
+        AZ::Render::ImageBasedLightFeatureProcessorInterface* iblFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::ImageBasedLightFeatureProcessorInterface>(m_entityContextId);
+        AZ::Render::DirectionalLightFeatureProcessorInterface* directionalLightFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::DirectionalLightFeatureProcessorInterface>(m_entityContextId);
+
+        AZ::Render::ExposureControlSettingsInterface* exposureControlSettingInterface = postProcessFeatureProcessor->GetOrCreateSettingsInterface(m_postProcessEntity->GetId())->GetOrCreateExposureControlSettingsInterface();
+
+        Camera::Configuration cameraConfig;
+        Camera::CameraRequestBus::EventResult(cameraConfig, m_cameraEntityId, &Camera::CameraRequestBus::Events::GetCameraConfiguration);
+
+        preset.ApplyLightingPreset(
+            iblFeatureProcessor,
+            skyboxFeatureProcessor,
+            exposureControlSettingInterface,
+            directionalLightFeatureProcessor,
+            cameraConfig,
+            m_lightHandles);
+    }
+
+    void CommonSampleComponentBase::OnTransformChanged(const AZ::Transform&, const AZ::Transform&)
+    {
+        const AZ::EntityId* currentBusId = AZ::TransformNotificationBus::GetCurrentBusId();
+        AZ::Render::DirectionalLightFeatureProcessorInterface* directionalLightFeatureProcessor = AZ::RPI::Scene::GetFeatureProcessorForEntityContextId<AZ::Render::DirectionalLightFeatureProcessorInterface>(m_entityContextId);
+        if (currentBusId && *currentBusId == m_cameraEntityId && directionalLightFeatureProcessor)
+        {
+            auto transform = AZ::Transform::CreateIdentity();
+            AZ::TransformBus::EventResult(
+                transform,
+                m_cameraEntityId,
+                &AZ::TransformBus::Events::GetWorldTM);
+            for (const AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle& handle : m_lightHandles)
+            {
+                directionalLightFeatureProcessor->SetCameraTransform(handle, transform);
+            }
+        }
+    }
+
+    void CommonSampleComponentBase::OnLightingPresetEntityShutdown(const AZ::EntityId& entityId)
+    {
+        if (m_postProcessEntity && m_postProcessEntity->GetId() == entityId)
+        {
+            m_postProcessEntity = nullptr;
+        }
+    }
+
+    void CommonSampleComponentBase::PreloadAssets(const AZStd::vector<AssetCollectionAsyncLoader::AssetToLoadInfo>& assetList)
+    {
+        m_isAllAssetsReady = false;
+
+        // Configure the imgui progress list widget.
+        auto onUserCancelledAction = [&]()
+        {
+            AZ_TracePrintf(m_sampleName.c_str() , "Cancelled by user.\n");
+            m_assetLoadManager.Cancel();
+            SampleComponentManagerRequestBus::Broadcast(&SampleComponentManagerRequests::Reset);
+        };
+        m_imguiProgressList.OpenPopup("Waiting For Assets...", "Assets pending for processing:", {}, onUserCancelledAction, true /*automaticallyCloseOnAction*/, "Cancel");
+
+        AZStd::for_each(assetList.begin(), assetList.end(),
+            [&](const AssetCollectionAsyncLoader::AssetToLoadInfo& item) { m_imguiProgressList.AddItem(item.m_assetPath); });
+
+        m_assetLoadManager.LoadAssetsAsync(assetList, [&](AZStd::string_view assetName, [[maybe_unused]] bool success, size_t pendingAssetCount)
+            {
+                AZ_Error(m_sampleName.c_str(), success, "Error loading asset %s, a crash will occur when OnAllAssetsReadyActivate() is called!", assetName.data());
+                AZ_TracePrintf(m_sampleName.c_str(), "Asset %s loaded %s. Wait for %zu more assets before full activation\n", assetName.data(), success ? "successfully" : "UNSUCCESSFULLY", pendingAssetCount);
+                m_imguiProgressList.RemoveItem(assetName);
+                if (!pendingAssetCount && !m_isAllAssetsReady)
+                {
+                    m_isAllAssetsReady = true;
+                    OnAllAssetsReadyActivate();
+                }
+            });
+    }
+
+} // namespace AtomSampleViewer

+ 122 - 0
Gem/Code/Source/CommonSampleComponentBase.h

@@ -0,0 +1,122 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/Component/Component.h>
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Component/EntityBus.h>
+#include <AzFramework/Entity/EntityContextBus.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessorInterface.h>
+#include <Atom/Feature/Utils/LightingPreset.h>
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+#include <Atom/Feature/PostProcess/PostProcessFeatureProcessorInterface.h>
+#include <Atom/Feature/ImageBasedLights/ImageBasedLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Utils/AssetCollectionAsyncLoader.h>
+
+#include <Utils/ImGuiProgressList.h>
+
+namespace AtomSampleViewer
+{
+    class CommonSampleComponentBase
+        : public AZ::Component
+        , public AZ::TransformNotificationBus::MultiHandler
+        , public AZ::EntityBus::MultiHandler
+    {
+    public:
+        AZ_TYPE_INFO(MaterialHotReloadTestComponent, "{7EECDF09-B774-46C1-AD6E-060CE5717C05}");
+
+        // AZ::Component overrides...
+        bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override;
+
+    protected:
+        //! Init and shut down should be called in derived components' Activate() and Deactivate().
+        //! @param loadDefaultLightingPresets if true, it will scan all lighting presets in the project and load them.
+        void InitLightingPresets(bool loadDefaultLightingPresets = false);
+        void ShutdownLightingPresets();
+
+        //! Add a drop down list to select lighting preset for this sample.
+        //! Lighting presets must be loaded before calling this function, otherwise the list will be hide.
+        //! It should be called between ImGui::Begin() and ImGui::End().
+        //! e.g. Calling it between ImGuiSidebar::Begin() and ImGuiSidebar::End() will embed this list into the side bar.
+        void ImGuiLightingPreset();
+
+        //! Load lighting presets from an asset.
+        //! It will clear any presets loaded previously.
+        void LoadLightingPresetsFromAsset(const AZStd::string& assetPath);
+
+        //! Load lighting presets from an asset.
+        //! Append the presets to the current existing presets.
+        void AppendLightingPresetsFromAsset(const AZStd::string& assetPath);
+
+        //! Clear all lighting presets.
+        void ClearLightingPresets();
+
+        //! Apply lighting presets to the scene.
+        //! Derived samples can override this function to have custom behaviors.
+        virtual void OnLightingPresetSelected(const AZ::Render::LightingPreset& preset);
+
+        //! Return the AtomSampleViewer EntityContextId, retrieved from the ComponentConfig
+        AzFramework::EntityContextId GetEntityContextId() const;
+
+        //! Return the AtomSampleViewer camera EntityId, retrieved from the ComponentConfig
+        AZ::EntityId GetCameraEntityId() const;
+
+        AZ::Render::MeshFeatureProcessorInterface* GetMeshFeatureProcessor() const;
+
+        void OnLightingPresetEntityShutdown(const AZ::EntityId& entityId);
+
+        // Preload assets 
+        void PreloadAssets(const AZStd::vector<AZ::AssetCollectionAsyncLoader::AssetToLoadInfo>& assetList);
+
+        //! Async asset load
+        AZ::AssetCollectionAsyncLoader m_assetLoadManager;
+
+        //! Showing the loading progress of preload assets
+        ImGuiProgressList m_imguiProgressList;
+
+        // The callback might be called more than one time if there are more than one asset are ready in one frame.
+        // Use this flag to prevent OnAllAssetsReadyActivate be called more than one time.
+        bool m_isAllAssetsReady = false;
+
+        AZStd::string m_sampleName;
+
+    private:
+        // AZ::TransformNotificationBus::MultiHandler overrides...
+        void OnTransformChanged(const AZ::Transform&, const AZ::Transform&) override;
+
+        // virtual call back function which is called when all preloaded assets are loaded.
+        virtual void OnAllAssetsReadyActivate() {};
+
+        AzFramework::EntityContextId m_entityContextId;
+        AZ::EntityId m_cameraEntityId;
+        mutable AZ::Render::MeshFeatureProcessorInterface* m_meshFeatureProcessor = nullptr;
+
+        //! All loaded lighting presets.
+        AZStd::vector<AZ::Render::LightingPreset> m_lightingPresets;
+
+        //! Lights created by lighting presets.
+        AZStd::vector<AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle> m_lightHandles;
+
+        //! Post process entity to handle ExposureControlSettings.
+        AZ::Entity* m_postProcessEntity = nullptr;
+
+        //! Dirty flag is set to true when m_lightingPresets is modified.
+        bool m_lightingPresetsDirty = true;
+
+        //! Current active lighting preset.
+        constexpr static int32_t InvalidLightingPresetIndex = -1;
+        int32_t m_currentLightingPresetIndex = InvalidLightingPresetIndex;
+    };
+
+} // namespace AtomSampleViewer

+ 523 - 0
Gem/Code/Source/CullingAndLodExampleComponent.cpp

@@ -0,0 +1,523 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <CullingAndLodExampleComponent.h>
+#include <SampleComponentConfig.h>
+
+#include <Automation/ScriptableImGui.h>
+#include <Atom/Component/DebugCamera/CameraControllerBus.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerBus.h>
+#include <imgui/imgui.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <AzCore/Asset/AssetManagerBus.h>
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Console/IConsole.h>
+#include <AzCore/Math/Transform.h>
+#include <AzCore/RTTI/BehaviorContext.h>
+#include <AzFramework/Components/CameraBus.h>
+#include <AzFramework/Components/TransformComponent.h>
+
+namespace AtomSampleViewer
+{
+    const AZ::Color CullingAndLodExampleComponent::DirectionalLightColor = AZ::Color::CreateOne();
+    const AZ::Render::ShadowmapSize CullingAndLodExampleComponent::s_shadowmapSizes[] =
+    {
+        AZ::Render::ShadowmapSize::Size256,
+        AZ::Render::ShadowmapSize::Size512,
+        AZ::Render::ShadowmapSize::Size1024,
+        AZ::Render::ShadowmapSize::Size2048
+    };
+    const char* CullingAndLodExampleComponent::s_directionalLightShadowmapSizeLabels[] =
+    {
+        "256",
+        "512",
+        "1024",
+        "2048"
+    };
+    const AZ::Render::ShadowFilterMethod CullingAndLodExampleComponent::s_shadowFilterMethods[] =
+    {
+        AZ::Render::ShadowFilterMethod::None,
+        AZ::Render::ShadowFilterMethod::Pcf,
+        AZ::Render::ShadowFilterMethod::Esm,
+        AZ::Render::ShadowFilterMethod::EsmPcf
+    };
+    const char* CullingAndLodExampleComponent::s_shadowFilterMethodLabels[] =
+    {
+        "None",
+        "PCF",
+        "ESM",
+        "ESM+PCF"
+    };
+
+    void CullingAndLodExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<CullingAndLodExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    void CullingAndLodExampleComponent::Activate()
+    {
+        using namespace AZ;
+
+        Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Enable, azrtti_typeid<Debug::NoClipControllerComponent>());
+
+        SaveCameraConfiguration();
+        ResetNoClipController();        
+
+        SetupScene();
+
+        m_imguiSidebar.Activate();
+
+        TickBus::Handler::BusConnect();
+    }
+
+    void CullingAndLodExampleComponent::Deactivate()
+    {
+        using namespace AZ;
+
+        TickBus::Handler::BusDisconnect();
+
+        m_imguiSidebar.Deactivate();
+
+        // disable camera control
+        RestoreCameraConfiguration();
+        Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Disable);
+
+        ClearMeshes();
+
+        m_directionalLightFeatureProcessor->ReleaseLight(m_directionalLightHandle);
+        UpdateSpotLightCount(0);
+    }
+
+    void CullingAndLodExampleComponent::OnTick(float deltaTime, AZ::ScriptTimePoint timePoint)
+    {
+        AZ_UNUSED(deltaTime);
+        AZ_UNUSED(timePoint);
+
+        using namespace AZ;
+
+        DrawSidebar();
+
+        // Pass camera data to the DirectionalLightFeatureProcessor
+        if (m_directionalLightHandle.IsValid())
+        {
+            Camera::Configuration config;
+            Camera::CameraRequestBus::EventResult(config, GetCameraEntityId(), &Camera::CameraRequestBus::Events::GetCameraConfiguration);
+            m_directionalLightFeatureProcessor->SetCameraConfiguration(m_directionalLightHandle, config);
+
+            Transform transform = Transform::CreateIdentity();
+            TransformBus::EventResult(transform, GetCameraEntityId(), &TransformBus::Events::GetWorldTM);
+            m_directionalLightFeatureProcessor->SetCameraTransform( m_directionalLightHandle, transform);
+        }
+    }
+
+    void CullingAndLodExampleComponent::ResetNoClipController()
+    {
+        using namespace AZ;
+        using namespace AZ::Debug;
+        Camera::CameraRequestBus::Event(GetCameraEntityId(), &Camera::CameraRequestBus::Events::SetFarClipDistance, 2000.0f);
+        NoClipControllerRequestBus::Event(GetCameraEntityId(), &NoClipControllerRequestBus::Events::SetPosition, Vector3(0.0f, -1.2f, 3.4f));
+        NoClipControllerRequestBus::Event(GetCameraEntityId(), &NoClipControllerRequestBus::Events::SetHeading, 0.0f);
+        NoClipControllerRequestBus::Event(GetCameraEntityId(), &NoClipControllerRequestBus::Events::SetPitch, 0.0f);
+    }
+
+    void CullingAndLodExampleComponent::SaveCameraConfiguration()
+    {
+        Camera::CameraRequestBus::EventResult(m_originalFarClipDistance, GetCameraEntityId(), &Camera::CameraRequestBus::Events::GetFarClipDistance);
+    }
+
+    void CullingAndLodExampleComponent::RestoreCameraConfiguration()
+    {
+        Camera::CameraRequestBus::Event(GetCameraEntityId(), &Camera::CameraRequestBus::Events::SetFarClipDistance, m_originalFarClipDistance);
+    }
+
+    void CullingAndLodExampleComponent::SetupScene()
+    {
+        using namespace AZ;
+
+        SpawnModelsIn2DGrid(5, 5);
+        SetupLights();
+    }
+
+    void CullingAndLodExampleComponent::ClearMeshes()
+    {
+        using namespace AZ;
+        Render::MeshFeatureProcessorInterface* meshFP = GetMeshFeatureProcessor();
+        for (auto& meshHandle : m_meshHandles)
+        {
+            meshFP->ReleaseMesh(meshHandle);
+        }
+        m_meshHandles.clear();
+    }
+
+    void CullingAndLodExampleComponent::SpawnModelsIn2DGrid(uint32_t numAlongXAxis, uint32_t numAlongYAxis)
+    {
+        using namespace AZ;
+        Render::MeshFeatureProcessorInterface* meshFP = GetMeshFeatureProcessor();
+
+        ClearMeshes();
+
+        const char objectModelFilename[] = "Objects/sphere_5lods.azmodel";
+        const char planeModelFilename[] = "Objects/plane.azmodel";
+        Data::Asset<RPI::ModelAsset> objectModelAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::ModelAsset>(
+            objectModelFilename, RPI::AssetUtils::TraceLevel::Assert);
+        Data::Asset<RPI::ModelAsset> planeModelAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::ModelAsset>(
+            planeModelFilename, RPI::AssetUtils::TraceLevel::Assert);
+        Data::Asset<RPI::MaterialAsset> materialAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::MaterialAsset>(
+            DefaultPbrMaterialPath, RPI::AssetUtils::TraceLevel::Assert);
+        Data::Instance<RPI::Material> material = RPI::Material::FindOrCreate(materialAsset);
+
+        float spacing = 2.0f*objectModelAsset->GetAabb().GetExtents().GetMaxElement();
+
+        for (uint32_t x = 0; x < numAlongXAxis; ++x)
+        {
+            for (uint32_t y = 0; y < numAlongYAxis; ++y)
+            {
+                auto meshHandle = meshFP->AcquireMesh(objectModelAsset, material);
+                Transform modelToWorld = Transform::CreateTranslation(Vector3(x * spacing, y * spacing, 2.0f));
+                meshFP->SetTransform(meshHandle, modelToWorld);
+                m_meshHandles.push_back(AZStd::move(meshHandle));
+            }
+        }
+
+        auto planeMeshHandle = meshFP->AcquireMesh(planeModelAsset, material);
+        Transform planeScale = Transform::CreateScale(Vector3(numAlongXAxis * spacing, numAlongYAxis * spacing, 1.0f));
+        Transform planeTranslation = Transform::CreateTranslation(Vector3(0.5f * numAlongXAxis * spacing, 0.5f * numAlongYAxis * spacing, 0.0f));
+        Transform planeModelToWorld = planeTranslation * planeScale;
+        meshFP->SetTransform(planeMeshHandle, planeModelToWorld);
+        m_meshHandles.push_back(AZStd::move(planeMeshHandle));
+    }
+
+    void CullingAndLodExampleComponent::SetupLights()
+    {
+        using namespace AZ;
+
+        m_directionalLightShadowmapSizeIndex = s_shadowmapSizeIndexDefault;
+        m_spotLightShadowmapSize = Render::ShadowmapSize::None; // random
+        m_cascadeCount = s_cascadesCountDefault;
+        m_ratioLogarithmUniform = s_ratioLogarithmUniformDefault;
+        m_spotLightCount = 0;
+
+        RPI::Scene* scene = RPI::Scene::GetSceneForEntityContextId(GetEntityContextId());
+        m_directionalLightFeatureProcessor = scene->GetFeatureProcessor<Render::DirectionalLightFeatureProcessorInterface>();
+        m_spotLightFeatureProcessor = scene->GetFeatureProcessor<Render::SpotLightFeatureProcessorInterface>();
+
+        // directional light
+        {
+            Render::DirectionalLightFeatureProcessorInterface* dirLightFP = m_directionalLightFeatureProcessor;
+            const DirectionalLightHandle handle = dirLightFP->AcquireLight();
+
+            const auto lightTransform = Transform::CreateLookAt(
+                Vector3(100, 100, 100),
+                Vector3::CreateZero());
+            dirLightFP->SetDirection(handle, lightTransform.GetBasis(1));
+
+            dirLightFP->SetRgbIntensity(handle, Render::PhotometricColor<Render::PhotometricUnit::Lux>(m_directionalLightIntensity * DirectionalLightColor));
+            dirLightFP->SetCascadeCount(handle, s_cascadesCountDefault);
+            dirLightFP->SetShadowmapSize(handle, s_shadowmapSizes[s_shadowmapSizeIndexDefault]);
+            dirLightFP->SetDebugFlags(handle,
+                m_isDebugColoringEnabled ?
+                AZ::Render::DirectionalLightFeatureProcessorInterface::DebugDrawFlags::DebugDrawAll :
+                AZ::Render::DirectionalLightFeatureProcessorInterface::DebugDrawFlags::DebugDrawNone);
+            dirLightFP->SetViewFrustumCorrectionEnabled(handle, m_isCascadeCorrectionEnabled);
+            dirLightFP->SetShadowFilterMethod(handle, s_shadowFilterMethods[m_shadowFilterMethodIndex]);
+            dirLightFP->SetShadowBoundaryWidth(handle, m_boundaryWidth);
+            dirLightFP->SetPredictionSampleCount(handle, m_predictionSampleCount);
+            dirLightFP->SetFilteringSampleCount(handle, m_filteringSampleCount);
+            dirLightFP->SetGroundHeight(handle, 0.f);
+
+            m_directionalLightHandle = handle;
+        }
+
+        // spot lights
+        {
+            m_spotLights.clear();
+            m_spotLights.reserve(SpotLightCountMax);
+            const float spotlightSpacing = 10.0f;
+            const int spotLightsPerRow = 10;
+            const Color colors[5] = {Colors::Red, Colors::Green, Colors::Blue, Colors::Orange, Colors::Pink};
+            for (int index = 0; index < SpotLightCountMax; ++index)
+            {
+                float xPos = (index % spotLightsPerRow) * spotlightSpacing;
+                float yPos = (index / spotLightsPerRow) * spotlightSpacing;
+                m_spotLights.emplace_back(
+                    colors[index % 5],
+                    Vector3(xPos, yPos, 10.0f),
+                    Vector3(1.0f, 1.0f, -1.0f).GetNormalized(),
+                    Render::ShadowmapSize::Size256);
+            }
+            UpdateSpotLightCount(20);
+        }
+    }
+
+    void CullingAndLodExampleComponent::UpdateSpotLightCount(uint16_t count)
+    {
+        using namespace AZ;
+
+        for (int index = count; index < m_spotLightCount; ++index)
+        {
+            SpotLightHandle& handle = m_spotLights[index].m_handle;
+            m_spotLightFeatureProcessor->ReleaseLight(handle);
+        }
+
+        const int previousSpotLightCount = m_spotLightCount;
+
+        for (int index = previousSpotLightCount; index < count; ++index)
+        {
+            Render::SpotLightFeatureProcessorInterface* const spotLightFP = m_spotLightFeatureProcessor;
+            const SpotLightHandle handle = spotLightFP->AcquireLight();
+            const SpotLight &spotLight = m_spotLights[index];
+
+            spotLightFP->SetPosition(handle, spotLight.m_position);
+            spotLightFP->SetDirection(handle, spotLight.m_direction);
+            spotLightFP->SetRgbIntensity(handle, Render::PhotometricColor<Render::PhotometricUnit::Candela>(spotLight.m_color * m_spotLightIntensity));
+            const float radius = sqrtf(m_spotLightIntensity / CutoffIntensity);
+            spotLightFP->SetAttenuationRadius(handle, radius);
+            spotLightFP->SetShadowmapSize(handle, m_spotLightShadowEnabled ? m_spotLights[index].m_shadowmapSize : Render::ShadowmapSize::None);
+            spotLightFP->SetConeAngles(handle, 45.f, 55.f);
+
+            m_spotLights[index].m_handle = handle;
+        }
+
+        m_spotLightCount = count;
+    }
+
+    void CullingAndLodExampleComponent::DrawSidebar()
+    {
+        using namespace AZ;
+        using namespace AZ::Render;
+
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        ImGui::Spacing();
+
+        if (ImGui::Button("Spawn 20x20 Grid of objects"))
+        {
+            SpawnModelsIn2DGrid(20, 20);
+        }
+        if (ImGui::Button("Spawn 50x50 Grid of objects"))
+        {
+            SpawnModelsIn2DGrid(50, 50);
+        }
+        if (ImGui::Button("Spawn 100x100 Grid of objects"))
+        {
+            SpawnModelsIn2DGrid(100, 100);
+        }
+
+        ImGui::Separator();        
+
+        ImGui::Text("Directional Light");
+        ImGui::Indent();
+        {
+            //SliderAngle() displays angles in degrees, but returns an angle in radians
+            ImGui::SliderAngle("Pitch", &m_directionalLightPitch, -90.0f, 0.f);
+            ImGui::SliderAngle("Yaw", &m_directionalLightYaw, 0.f, 360.f);
+            const auto lightTrans = Transform::CreateRotationZ(m_directionalLightYaw) * Transform::CreateRotationX(m_directionalLightPitch);
+            m_directionalLightFeatureProcessor->SetDirection(m_directionalLightHandle, lightTrans.GetBasis(1));
+
+            if (ImGui::SliderFloat("Intensity##directional", &m_directionalLightIntensity, 0.f, 20.f, "%.1f", 2.f))
+            {
+                m_directionalLightFeatureProcessor->SetRgbIntensity(
+                    m_directionalLightHandle,
+                    Render::PhotometricColor<Render::PhotometricUnit::Lux>(DirectionalLightColor * m_directionalLightIntensity));
+            }
+
+            ImGui::Separator();
+
+            ImGui::Text("Shadowmap Size");
+            if (ImGui::Combo( "Size", &m_directionalLightShadowmapSizeIndex, s_directionalLightShadowmapSizeLabels, AZ_ARRAY_SIZE(s_directionalLightShadowmapSizeLabels)))
+            {
+                m_directionalLightFeatureProcessor->SetShadowmapSize(m_directionalLightHandle, s_shadowmapSizes[m_directionalLightShadowmapSizeIndex]);
+            }
+
+            ImGui::Text("Number of cascades");
+            bool cascadesChanged = false;
+            cascadesChanged = cascadesChanged ||
+                ImGui::RadioButton("1", &m_cascadeCount, 1);
+            ImGui::SameLine();
+            cascadesChanged = cascadesChanged ||
+                ImGui::RadioButton("2", &m_cascadeCount, 2);
+            ImGui::SameLine();
+            cascadesChanged = cascadesChanged ||
+                ImGui::RadioButton("3", &m_cascadeCount, 3);
+            ImGui::SameLine();
+            cascadesChanged = cascadesChanged ||
+                ImGui::RadioButton("4", &m_cascadeCount, 4);
+            if (cascadesChanged)
+            {
+                m_directionalLightFeatureProcessor->SetCascadeCount(m_directionalLightHandle, m_cascadeCount);
+            }
+
+            ImGui::Spacing();
+
+            ImGui::Text("Cascade partition scheme");
+            ImGui::Text("  (uniform <--> logarithm)");
+            if (ImGui::SliderFloat("Ratio", &m_ratioLogarithmUniform, 0.f, 1.f, "%0.3f"))
+            {
+                m_directionalLightFeatureProcessor->SetShadowmapFrustumSplitSchemeRatio(m_directionalLightHandle, m_ratioLogarithmUniform);
+            }
+
+            ImGui::Spacing();
+
+            if (ImGui::Checkbox("Cascade Position Correction", &m_isCascadeCorrectionEnabled))
+            {
+                m_directionalLightFeatureProcessor->SetViewFrustumCorrectionEnabled(m_directionalLightHandle, m_isCascadeCorrectionEnabled);
+            }
+
+            if (ImGui::Checkbox("Debug Coloring", &m_isDebugColoringEnabled))
+            {
+                m_directionalLightFeatureProcessor->SetDebugFlags(m_directionalLightHandle,
+                    m_isDebugColoringEnabled ?
+                    AZ::Render::DirectionalLightFeatureProcessorInterface::DebugDrawFlags::DebugDrawAll :
+                    AZ::Render::DirectionalLightFeatureProcessorInterface::DebugDrawFlags::DebugDrawNone);
+            }
+
+            ImGui::Spacing();
+
+            ImGui::Text("Filtering");
+            if (ImGui::Combo("Filter Method", &m_shadowFilterMethodIndex, s_shadowFilterMethodLabels, AZ_ARRAY_SIZE(s_shadowFilterMethodLabels)))
+            {
+                m_directionalLightFeatureProcessor->SetShadowFilterMethod(m_directionalLightHandle, s_shadowFilterMethods[m_shadowFilterMethodIndex]);
+            }
+            ImGui::Text("Boundary Width in meter");
+            if (ImGui::SliderFloat("Width", &m_boundaryWidth, 0.f, 0.1f, "%.3f"))
+            {
+                m_directionalLightFeatureProcessor->SetShadowBoundaryWidth(m_directionalLightHandle, m_boundaryWidth);
+            }
+
+            ImGui::Spacing();
+            ImGui::Text("Filtering (PCF specific)");
+            if (ImGui::SliderInt("Prediction #", &m_predictionSampleCount, 4, 16))
+            {
+                m_directionalLightFeatureProcessor->SetPredictionSampleCount(m_directionalLightHandle, m_predictionSampleCount);
+            }
+            if (ImGui::SliderInt("Filtering #", &m_filteringSampleCount, 4, 64))
+            {
+                m_directionalLightFeatureProcessor->SetFilteringSampleCount(m_directionalLightHandle, m_filteringSampleCount);
+            }
+        }
+        ImGui::Unindent();
+
+        ImGui::Separator();
+
+        ImGui::Text("Spot Lights");
+        ImGui::Indent();
+        {
+            int spotLightCount = m_spotLightCount;
+            if (ImGui::SliderInt("Number", &spotLightCount, 0, SpotLightCountMax))
+            {
+                UpdateSpotLightCount(spotLightCount);
+            }
+
+            if (ImGui::SliderFloat("Intensity##spot", &m_spotLightIntensity, 0.f, 100000.f, "%.1f", 4.f))
+            {
+                for (const SpotLight& light : m_spotLights)
+                {
+                    if (light.m_handle.IsValid())
+                    {
+                        m_spotLightFeatureProcessor->SetRgbIntensity(light.m_handle, Render::PhotometricColor<Render::PhotometricUnit::Candela>(light.m_color * m_spotLightIntensity));
+                        const float radius = sqrtf(m_spotLightIntensity / CutoffIntensity);
+                        m_spotLightFeatureProcessor->SetAttenuationRadius(light.m_handle, radius);
+                    }
+                }
+            }
+
+            bool spotLightShadowmapChanged = ImGui::Checkbox("Enable Shadow", &m_spotLightShadowEnabled);
+            
+            ImGui::Text("Shadowmap Size");
+            int newSize = static_cast<int>(m_spotLightShadowmapSize);
+            // To avoid GPU memory consumption, we avoid bigger shadowmap sizes here.
+            spotLightShadowmapChanged = spotLightShadowmapChanged ||
+                ImGui::RadioButton("256", &newSize, static_cast<int>(Render::ShadowmapSize::Size256)) ||
+                ImGui::RadioButton("512", &newSize, static_cast<int>(Render::ShadowmapSize::Size512)) ||
+                ImGui::RadioButton("Random", &newSize, static_cast<int>(Render::ShadowmapSize::None));
+
+            if (spotLightShadowmapChanged)
+            {
+                m_spotLightShadowmapSize = static_cast<Render::ShadowmapSize>(newSize);
+                UpdateSpotLightShadowmapSize();
+            }
+        }
+        ImGui::Unindent();
+
+        // For automated screenshot verification testing: force the camera transform and turn on the debug window so the cull stats show up in the screenshot(s)
+        if (ScriptableImGui::Button("Begin Verification"))
+        {
+            Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Disable);
+            Transform tm = Transform::CreateTranslation(Vector3(3.0f, -16.0f, 6.0f));
+            TransformBus::Event(GetCameraEntityId(), &TransformBus::Events::SetWorldTM, tm);
+        }
+        if (ScriptableImGui::Button("End Verification"))
+        {
+            Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Enable, azrtti_typeid<Debug::NoClipControllerComponent>());
+        }
+
+        m_imguiSidebar.End();
+    }
+
+    void CullingAndLodExampleComponent::UpdateSpotLightShadowmapSize()
+    {
+        using namespace AZ::Render;
+        SpotLightFeatureProcessorInterface* const featureProcessor = m_spotLightFeatureProcessor;
+
+        if (!m_spotLightShadowEnabled)
+        {
+            // disabled shadows
+            for (const SpotLight& light : m_spotLights)
+            {
+                if (light.m_handle.IsValid())
+                {
+                    featureProcessor->SetShadowmapSize(
+                        light.m_handle,
+                        ShadowmapSize::None);
+                }
+            }
+        }
+        else if (m_spotLightShadowmapSize != ShadowmapSize::None)
+        {
+            // uniform size
+            for (const SpotLight& light : m_spotLights)
+            {
+                if (light.m_handle.IsValid())
+                {
+                    featureProcessor->SetShadowmapSize(
+                        light.m_handle,
+                        m_spotLightShadowmapSize);
+                }
+            }
+        }
+        else
+        {
+            // random sizes
+            for (const SpotLight& light : m_spotLights)
+            {
+                if (light.m_handle.IsValid())
+                {
+                    featureProcessor->SetShadowmapSize(
+                        light.m_handle,
+                        light.m_shadowmapSize);
+                }
+            }
+        }
+    }
+
+} // namespace AtomSampleViewer

+ 140 - 0
Gem/Code/Source/CullingAndLodExampleComponent.h

@@ -0,0 +1,140 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/ShadowConstants.h>
+#include <Atom/Feature/CoreLights/SpotLightFeatureProcessorInterface.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessor.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/Math/Aabb.h>
+#include <AzCore/Math/Random.h>
+#include <AzFramework/Entity/EntityContext.h>
+#include <Utils/ImGuiSidebar.h>
+
+namespace AtomSampleViewer
+{
+    class CullingAndLodExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(CullingAndLodExampleComponent, "CA7AB736-5C80-425E-8DF3-E1C22971D79C", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        CullingAndLodExampleComponent() = default;
+        ~CullingAndLodExampleComponent() override = default;
+
+        // need to remove the copy constructor because Handles cannot be copied, only moved
+        CullingAndLodExampleComponent(CullingAndLodExampleComponent&) = delete;
+
+        // AZ::Component overrides...
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        using DirectionalLightHandle = AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle;
+        using SpotLightHandle = AZ::Render::SpotLightFeatureProcessorInterface::LightHandle;
+
+        class SpotLight
+        {
+        public:
+            SpotLight() = delete;
+            explicit SpotLight(const AZ::Color& color, const AZ::Vector3& position,
+                const AZ::Vector3& direction, AZ::Render::ShadowmapSize shadowmapSize)
+                : m_color(color)
+                , m_position(position)
+                , m_direction(direction)
+                , m_shadowmapSize(shadowmapSize)
+            {}
+            ~SpotLight() = default;
+
+            const AZ::Color m_color;
+            const AZ::Vector3 m_position;
+            const AZ::Vector3 m_direction;
+            const AZ::Render::ShadowmapSize m_shadowmapSize;
+            SpotLightHandle m_handle;
+        };
+
+        static constexpr int SpotLightCountMax = 100;
+        static constexpr int SpotLightCountDefault = 10;
+        static constexpr float CutoffIntensity = 0.5f;
+
+        static const AZ::Color DirectionalLightColor;
+        
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        void ResetNoClipController();
+
+        void SaveCameraConfiguration();
+        void RestoreCameraConfiguration();
+
+        void SetupScene();
+        void ClearMeshes();
+        void SpawnModelsIn2DGrid(uint32_t numAlongXAxis, uint32_t numAlongYAxis);
+        void SetupLights();
+        void UpdateSpotLightCount(uint16_t count);
+
+        void DrawSidebar();
+        void UpdateSpotLightShadowmapSize();
+
+        float m_originalFarClipDistance = 0.f;
+
+        // lights
+        AZ::Render::DirectionalLightFeatureProcessorInterface* m_directionalLightFeatureProcessor = nullptr;
+        AZ::Render::SpotLightFeatureProcessorInterface* m_spotLightFeatureProcessor = nullptr;
+        DirectionalLightHandle m_directionalLightHandle;
+        AZStd::vector<SpotLight> m_spotLights;
+
+        // models
+        AZStd::vector<AZ::Render::MeshFeatureProcessorInterface::MeshHandle> m_meshHandles;
+        AZStd::vector<AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler> m_modelChangedHandlers;
+
+        // GUI
+        ImGuiSidebar m_imguiSidebar;
+        float m_directionalLightPitch = -1.22;
+        float m_directionalLightYaw = 0.7f;
+        float m_directionalLightIntensity = 4.f;
+        float m_spotLightIntensity = 2000.f;
+        int m_spotLightCount = 0;
+
+        // Shadowmap
+        static const AZ::Render::ShadowmapSize s_shadowmapSizes[];
+        static const char* s_directionalLightShadowmapSizeLabels[];
+        static constexpr int s_shadowmapSizeIndexDefault = 3;
+        static constexpr int s_cascadesCountDefault = 4;
+        static constexpr float s_ratioLogarithmUniformDefault = 0.8f;
+        int m_directionalLightShadowmapSizeIndex = 0;
+        int m_cascadeCount = 0;
+        float m_ratioLogarithmUniform = 0.f;
+        AZ::Render::ShadowmapSize m_spotLightShadowmapSize = AZ::Render::ShadowmapSize::None;
+        bool m_spotLightShadowEnabled = true;
+
+        // Edge-softening of directional light shadows
+        static const AZ::Render::ShadowFilterMethod s_shadowFilterMethods[];
+        static const char* s_shadowFilterMethodLabels[];
+        int m_shadowFilterMethodIndex = 0; // filter method is None.
+        float m_boundaryWidth = 0.03f; // 3cm
+        int m_predictionSampleCount = 4;
+        int m_filteringSampleCount = 16;
+
+        bool m_isCascadeCorrectionEnabled = false;
+        bool m_isDebugColoringEnabled = false;
+    };
+} // namespace AtomSampleViewer

+ 116 - 0
Gem/Code/Source/DecalContainer.cpp

@@ -0,0 +1,116 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include "DecalContainer.h"
+#include <AzCore/Math/Vector3.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+
+
+namespace AtomSampleViewer
+{
+    namespace
+    {
+        static const char* const DecalMaterialNames[] =
+        {
+            "materials/Decal/scorch_01_decal.azmaterial",
+            "materials/Decal/brushstoke_01_decal.azmaterial",
+            "materials/Decal/am_road_dust_decal.azmaterial",
+            "materials/Decal/am_mud_decal.azmaterial",
+            "materials/Decal/airship_nose_number_decal.azmaterial",
+            "materials/Decal/airship_tail_01_decal.azmaterial",
+            "materials/Decal/airship_tail_02_decal.azmaterial",
+            "materials/Decal/airship_symbol_decal.azmaterial",
+        };
+    }
+
+    DecalContainer::DecalContainer(AZ::Render::DecalFeatureProcessorInterface* fp)
+        : m_decalFeatureProcessor(fp)
+    {
+        SetupDecals();
+    }
+
+    void DecalContainer::SetupDecals()
+    {
+        const float HalfLength = 0.25f;
+        const float HalfProjectionDepth = 10.0f;
+        const AZ::Vector3 halfSize(HalfLength, HalfLength, HalfProjectionDepth);
+        SetupNewDecal(AZ::Vector3(-0.75f, -0.25f, 1), halfSize, DecalMaterialNames[0]);
+        SetupNewDecal(AZ::Vector3(-0.25f, -0.25f, 1), halfSize, DecalMaterialNames[1]);
+        SetupNewDecal(AZ::Vector3(0.25f, -0.25f, 1), halfSize, DecalMaterialNames[2]);
+        SetupNewDecal(AZ::Vector3(0.75f, -0.25f, 1), halfSize, DecalMaterialNames[3]);
+        SetupNewDecal(AZ::Vector3(-0.75f, 0.25f, 1), halfSize, DecalMaterialNames[4]);
+        SetupNewDecal(AZ::Vector3(-0.25f, 0.25f, 1), halfSize, DecalMaterialNames[5]);
+        SetupNewDecal(AZ::Vector3(0.25f, 0.25f, 1), halfSize, DecalMaterialNames[6]);
+        SetupNewDecal(AZ::Vector3(0.75f, 0.25f, 1), halfSize, DecalMaterialNames[7]);
+    }
+
+    DecalContainer::~DecalContainer()
+    {
+        SetNumDecalsActive(0);
+    }
+
+    void DecalContainer::SetNumDecalsActive(int numDecals)
+    {
+        for (int i = 0; i < aznumeric_cast<int>(m_decals.size()); ++i)
+        {
+            if (i < numDecals)
+            {
+                AcquireDecal(i);
+            }
+            else
+            {
+                ReleaseDecal(i);
+            }
+        }
+        m_numDecalsActive = numDecals;
+    }
+
+    void DecalContainer::SetupNewDecal(const AZ::Vector3 position, const AZ::Vector3 halfSize, const char* const decalMaterialName)
+    {
+        Decal newDecal;
+        newDecal.m_position = position;
+        newDecal.m_halfSize = halfSize;
+        newDecal.m_materialName = decalMaterialName;
+
+        m_decals.push_back(newDecal);
+    }
+
+    void DecalContainer::AcquireDecal(int i)
+    {
+        Decal& decal = m_decals[i];
+        if (decal.m_decalHandle.IsValid())
+        {
+            return;
+        }
+
+        decal.m_decalHandle = m_decalFeatureProcessor->AcquireDecal();
+        m_decalFeatureProcessor->SetDecalHalfSize(decal.m_decalHandle, decal.m_halfSize);
+        m_decalFeatureProcessor->SetDecalPosition(decal.m_decalHandle, decal.m_position);
+
+        const AZ::Data::AssetId assetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(decal.m_materialName);
+        m_decalFeatureProcessor->SetDecalMaterial(decal.m_decalHandle, assetId);
+    }
+
+    void DecalContainer::ReleaseDecal(int i)
+    {
+        Decal& decal = m_decals[i];
+        if (decal.m_decalHandle.IsNull())
+        {
+            return;
+        }
+
+        m_decalFeatureProcessor->ReleaseDecal(decal.m_decalHandle);
+        decal.m_decalHandle = AZ::Render::DecalFeatureProcessorInterface::DecalHandle::Null;
+    }
+}

+ 56 - 0
Gem/Code/Source/DecalContainer.h

@@ -0,0 +1,56 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/Math/Quaternion.h>
+#include <Atom/Feature/Decals/DecalFeatureProcessorInterface.h>
+
+
+namespace AtomSampleViewer
+{
+    //! Helper class used by the AtomSampleViewer examples.
+    //! Stores a list of decals and will automatically release them upon destruction of the container.
+    class DecalContainer
+    {
+    public:
+
+        DecalContainer(AZ::Render::DecalFeatureProcessorInterface* fp);
+        DecalContainer(const DecalContainer&) = delete;
+        DecalContainer& operator=(const DecalContainer&) = delete;
+        ~DecalContainer();
+
+        void SetNumDecalsActive(int numDecals);
+        int GetMaxDecals() const { return aznumeric_cast<int>(m_decals.size()); }
+        int GetNumDecalsActive() const { return m_numDecalsActive; }
+
+    private:
+
+        void SetupDecals();
+        void SetupNewDecal(const AZ::Vector3 position, const AZ::Vector3 halfSize, const char* const decalMaterialName);
+        void AcquireDecal(int i);
+        void ReleaseDecal(int i);
+
+        struct Decal
+        {
+            AZ::Vector3 m_position;
+            AZ::Vector3 m_halfSize;
+            AZ::Quaternion m_quaternion;
+            const char* m_materialName = nullptr;
+            AZ::Render::DecalFeatureProcessorInterface::DecalHandle m_decalHandle;
+        };
+
+        AZStd::vector<Decal> m_decals;
+        AZ::Render::DecalFeatureProcessorInterface* m_decalFeatureProcessor = nullptr;
+        int m_numDecalsActive = 0;
+    };
+} // namespace AtomSampleViewer

+ 144 - 0
Gem/Code/Source/DecalExampleComponent.cpp

@@ -0,0 +1,144 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <DecalExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+
+#include <Atom/RPI.Public/View.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    void DecalExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class < DecalExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    void DecalExampleComponent::Activate()
+    {
+        m_sampleName = "DecalExampleComponent";
+
+        CreateDecalContainer();
+        m_decalContainer->SetNumDecalsActive(m_decalContainer->GetMaxDecals());
+
+        m_imguiSidebar.Activate();
+
+        // List of all assets this example needs.
+        AZStd::vector<AZ::AssetCollectionAsyncLoader::AssetToLoadInfo> assetList = {
+            {"objects/plane.azmodel", azrtti_typeid<AZ::RPI::ModelAsset>()}, // The model
+        };
+
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScript);
+
+        PreloadAssets(assetList);
+    }
+
+    void DecalExampleComponent::OnAllAssetsReadyActivate()
+    {
+        CreatePlaneObject();
+        EnableArcBallCameraController();
+        ConfigureCameraToLookDownAtObject();
+        AddImageBasedLight();
+
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void DecalExampleComponent::CreatePlaneObject()
+    {
+        auto meshAsset = m_assetLoadManager.GetAsset<AZ::RPI::ModelAsset>("objects/plane.azmodel");
+        m_meshHandle = GetMeshFeatureProcessor()->AcquireMesh(meshAsset);
+        ScaleObjectToFitDecals();
+    }
+
+    void DecalExampleComponent::ScaleObjectToFitDecals()
+    {
+        const AZ::Transform doubleSize = AZ::Transform::CreateScale(AZ::Vector3(2.0f, 1.0f, 1.0f));
+        GetMeshFeatureProcessor()->SetTransform(m_meshHandle, doubleSize);
+    }
+
+    void DecalExampleComponent::Deactivate()
+    {
+        m_decalContainer = nullptr;
+        AZ::TickBus::Handler::BusDisconnect();
+        m_imguiSidebar.Deactivate();
+        m_defaultIbl.Reset();
+        GetMeshFeatureProcessor()->ReleaseMesh(m_meshHandle);
+    }
+
+    void DecalExampleComponent::AddImageBasedLight()
+    {
+        m_defaultIbl.Init(AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get());
+    }
+
+    void DecalExampleComponent::EnableArcBallCameraController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::ArcBallControllerComponent>());
+    }
+
+    void DecalExampleComponent::ConfigureCameraToLookDownAtObject()
+    {
+        const AZ::Vector3 CameraPanOffet(0.0f, 0.5f, -0.5f);
+        const float CameraDistance = 1.5f;
+        const float CameraPitch = -0.8f;
+
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetPan, CameraPanOffet);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetPitch, CameraPitch);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetDistance, CameraDistance);
+    }
+
+    void DecalExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        DrawSidebar();
+    }
+
+    void DecalExampleComponent::DrawSidebar()
+    {
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+        int numDecalsActive = m_decalContainer->GetNumDecalsActive();
+        if (ScriptableImGui::SliderInt("Point count", &numDecalsActive, 0, m_decalContainer->GetMaxDecals()))
+        {
+            m_decalContainer->SetNumDecalsActive(numDecalsActive);
+        }
+
+        m_imguiSidebar.End();
+    }
+
+    void DecalExampleComponent::CreateDecalContainer()
+    {
+        const AZ::RPI::Scene* scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get();
+        const auto decalFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DecalFeatureProcessorInterface>();
+        m_decalContainer = AZStd::make_unique<DecalContainer>(decalFeatureProcessor);
+    }
+
+}

+ 66 - 0
Gem/Code/Source/DecalExampleComponent.h

@@ -0,0 +1,66 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <AzFramework/Input/Events/InputChannelEventListener.h>
+#include <Atom/Feature/Decals/DecalFeatureProcessorInterface.h>
+#include <Utils/Utils.h>
+#include <Utils/ImGuiSidebar.h>
+#include "DecalContainer.h"
+
+namespace AtomSampleViewer
+{
+    class DecalContainer;
+
+    //! This component creates a simple scene to test Atom's decal system.
+    class DecalExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+
+        AZ_COMPONENT(DecalExampleComponent, "{91CFCFFC-EDD9-47EB-AE98-4BE9617D6F2F}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        void Activate() override;
+
+
+        void Deactivate() override;
+
+    private:
+
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        void CreateDecalContainer();
+        void CreatePlaneObject();
+        void ScaleObjectToFitDecals();
+        void EnableArcBallCameraController();
+        void ConfigureCameraToLookDownAtObject();
+        void AddImageBasedLight();
+        void DrawSidebar();
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+        Utils::DefaultIBL m_defaultIbl;
+        AZStd::unique_ptr<DecalContainer> m_decalContainer;
+        ImGuiSidebar m_imguiSidebar;
+
+        // CommonSampleComponentBase overrides...
+        void OnAllAssetsReadyActivate() override;
+    };
+} // namespace AtomSampleViewer

+ 389 - 0
Gem/Code/Source/DepthOfFieldExampleComponent.cpp

@@ -0,0 +1,389 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <DepthOfFieldExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+#include <Atom/RPI.Public/Model/Model.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+
+#include <AzCore/Component/Entity.h>
+#include <AzFramework/Components/CameraBus.h>
+#include <AzFramework/Components/TransformComponent.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <EntityUtilityFunctions.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    void DepthOfFieldExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<DepthOfFieldExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    void DepthOfFieldExampleComponent::Activate()
+    {
+        using namespace AZ;
+
+        RPI::Scene* scene = RPI::RPISystemInterface::Get()->GetDefaultScene().get();
+        m_postProcessFeatureProcessor = scene->GetFeatureProcessor<Render::PostProcessFeatureProcessorInterface>();
+        m_directionalLightFeatureProcessor = scene->GetFeatureProcessor<Render::DirectionalLightFeatureProcessorInterface>();
+
+        // Create the assets
+        m_bunnyModelAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::ModelAsset>("objects/bunny.azmodel", RPI::AssetUtils::TraceLevel::Assert);
+        m_materialAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::MaterialAsset>("shaders/staticmesh.azmaterial", RPI::AssetUtils::TraceLevel::Assert);
+
+        CreateMeshes();
+        CreateLight();
+        CreateDepthOfFieldEntity();
+        UseArcBallCameraController();
+
+        m_imguiSidebar.Activate();
+
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void DepthOfFieldExampleComponent::Deactivate()
+    {
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetNearClipDistance,
+            m_saveDefaultNearOnAtomSampleViewer);
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFarClipDistance,
+            m_saveDefaultFarOnAtomSampleViewer);
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFovDegrees,
+            m_saveDefaultFovDegreesOnAtomSampleViewer);
+
+        AZ::TickBus::Handler::BusDisconnect();
+        RemoveController();
+
+        AZ::EntityBus::MultiHandler::BusDisconnect();
+
+        if (m_postProcessSettings)
+        {
+            m_postProcessSettings->RemoveDepthOfFieldSettingsInterface();
+        }
+        m_postProcessSettings = nullptr;
+
+        if (m_depthOfFieldEntity)
+        {
+            DestroyEntity(m_depthOfFieldEntity, GetEntityContextId());
+        }
+
+        if (m_directionalLightHandle.IsValid())
+        {
+            m_directionalLightFeatureProcessor->ReleaseLight(m_directionalLightHandle);
+        }
+
+        for (MeshHandle& meshHandle : m_meshHandles)
+        {
+            GetMeshFeatureProcessor()->ReleaseMesh(meshHandle);
+        }
+
+        m_imguiSidebar.Deactivate();
+    }
+
+    void DepthOfFieldExampleComponent::OnTick([[maybe_unused]] float deltaTime, AZ::ScriptTimePoint)
+    {
+        if (!m_isInitCamera)
+        {
+            const AZ::Vector3 InitTranslation = DistanceBetweenUnits * AZ::Vector3(BunnyNumber / 8, BunnyNumber / 16, 0.5f);
+            constexpr float InitPitch = 0.0f;
+            constexpr float InitHeading = -0.65f;
+            AZ::Debug::ArcBallControllerRequestBus::Event(
+                GetCameraEntityId(),
+                &AZ::Debug::ArcBallControllerRequestBus::Events::SetCenter,
+                InitTranslation);
+            AZ::Debug::ArcBallControllerRequestBus::Event(
+                GetCameraEntityId(),
+                &AZ::Debug::ArcBallControllerRequestBus::Events::SetPitch,
+                InitPitch);
+            AZ::Debug::ArcBallControllerRequestBus::Event(
+                GetCameraEntityId(),
+                &AZ::Debug::ArcBallControllerRequestBus::Events::SetHeading,
+                InitHeading);
+        }
+        m_isInitCamera = true;
+
+        DrawSidebar();
+    }
+
+    void DepthOfFieldExampleComponent::OnEntityDestruction(const AZ::EntityId& entityId)
+    {
+        AZ::EntityBus::MultiHandler::BusDisconnect(entityId);
+
+        if (m_depthOfFieldEntity && m_depthOfFieldEntity->GetId() == entityId) 
+        {
+            m_postProcessFeatureProcessor->RemoveSettingsInterface(m_depthOfFieldEntity->GetId());
+            m_depthOfFieldEntity = nullptr;
+        }
+        else
+        {
+            AZ_Assert(false, "unexpected entity destruction is signaled.");
+        }
+    }
+
+    void DepthOfFieldExampleComponent::UseArcBallCameraController()
+    {
+        using namespace AZ;
+        Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<Debug::ArcBallControllerComponent>());
+
+        Camera::CameraRequestBus::EventResult(
+            m_saveDefaultNearOnAtomSampleViewer,
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::GetNearClipDistance);
+        Camera::CameraRequestBus::EventResult(
+            m_saveDefaultFarOnAtomSampleViewer,
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::GetFarClipDistance);
+        Camera::CameraRequestBus::EventResult(
+            m_saveDefaultFovDegreesOnAtomSampleViewer,
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::GetFovDegrees);
+
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetNearClipDistance,
+            ViewNear);
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFarClipDistance,
+            ViewFar);
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFovDegrees,
+            ViewFovDegrees);
+    }
+
+    void DepthOfFieldExampleComponent::RemoveController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+    }
+
+    void DepthOfFieldExampleComponent::CreateMeshes()
+    {
+        using namespace AZ;
+
+        Render::MaterialAssignmentMap materials;
+        Render::MaterialAssignment& defaultMaterial = materials[AZ::Render::DefaultMaterialAssignmentId];
+        defaultMaterial.m_materialAsset = m_materialAsset;
+        defaultMaterial.m_materialInstance = RPI::Material::FindOrCreate(defaultMaterial.m_materialAsset);
+
+        Vector3 translation = Vector3::CreateZero();
+        Transform scaleTransform = Transform::CreateScale(AZ::Vector3(ModelScaleRatio));
+
+        for (MeshHandle& meshHandle : m_meshHandles)
+        {
+            meshHandle = GetMeshFeatureProcessor()->AcquireMesh(m_bunnyModelAsset, materials);
+
+            auto transform = AZ::Transform::CreateTranslation(translation);
+            transform *= scaleTransform;
+            GetMeshFeatureProcessor()->SetTransform(meshHandle, transform);
+
+            translation += Vector3(DistanceBetweenUnits, DistanceBetweenUnits, 0.0f);
+        }
+    }
+
+    void DepthOfFieldExampleComponent::CreateLight()
+    {
+        using namespace AZ;
+        Render::DirectionalLightFeatureProcessorInterface* const fp = m_directionalLightFeatureProcessor;
+
+        DirectionalLightHandle handle = fp->AcquireLight();
+        fp->SetRgbIntensity(handle, Render::PhotometricColor<Render::PhotometricUnit::Lux>(Color(5.f, 5.f, 5.f, 1.f)));
+        fp->SetDirection(handle, Vector3(-1.f, 1.f, -1.f).GetNormalized());
+        m_directionalLightHandle = handle;
+    }
+
+    void DepthOfFieldExampleComponent::CreateDepthOfFieldEntity()
+    {
+        using namespace AZ;
+        m_depthOfFieldEntity = CreateEntity("DepthOfField", GetEntityContextId());
+
+        // Transform
+        Component* transformComponent = nullptr;
+        ComponentDescriptorBus::EventResult(
+            transformComponent,
+            azrtti_typeid<AzFramework::TransformComponent>(),
+            &ComponentDescriptorBus::Events::CreateComponent);
+        m_depthOfFieldEntity->AddComponent(transformComponent);
+
+        // DepthOfField
+        m_postProcessSettings = m_postProcessFeatureProcessor->GetOrCreateSettingsInterface(m_depthOfFieldEntity->GetId());
+        m_depthOfFieldSettings = m_postProcessSettings->GetOrCreateDepthOfFieldSettingsInterface();
+        m_depthOfFieldSettings->SetQualityLevel(1);
+        m_depthOfFieldSettings->SetApertureF(0.5f);
+        m_depthOfFieldSettings->SetFocusDistance(FocusDefault);
+        m_depthOfFieldSettings->SetEnableDebugColoring(false);
+        m_depthOfFieldSettings->SetEnableAutoFocus(false);
+        m_depthOfFieldSettings->SetAutoFocusScreenPosition(AZ::Vector2{ 0.5f, 0.5f });
+        m_depthOfFieldSettings->SetAutoFocusSensitivity(1.0f);
+        m_depthOfFieldSettings->SetAutoFocusSpeed(Render::DepthOfField::AutoFocusSpeedMax);
+        m_depthOfFieldSettings->SetAutoFocusDelay(0.2f);
+        m_depthOfFieldSettings->SetCameraEntityId(GetCameraEntityId());
+        m_depthOfFieldSettings->SetEnabled(true);
+        m_depthOfFieldSettings->OnConfigChanged();
+
+        m_depthOfFieldEntity->Activate();
+        AZ::EntityBus::MultiHandler::BusConnect(m_depthOfFieldEntity->GetId());
+    }
+
+    void DepthOfFieldExampleComponent::DrawSidebar()
+    {
+        using namespace AZ::Render;
+
+        if (GetCameraEntityId() != m_depthOfFieldSettings->GetCameraEntityId())
+        {
+            m_depthOfFieldSettings->SetCameraEntityId(GetCameraEntityId());
+            m_depthOfFieldSettings->OnConfigChanged();
+        }
+
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        ImGui::Spacing();
+
+        ImGui::Text("Depth of Field");
+        ImGui::Indent();
+        {
+            int32_t qualityLevel = aznumeric_cast<int32_t>(m_depthOfFieldSettings->GetQualityLevel());
+            if (ImGui::SliderInt("Quality Level", &qualityLevel, 0, DepthOfField::QualityLevelMax - 1, "%d") || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetQualityLevel(aznumeric_cast<uint32_t>(qualityLevel));
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+
+            float apertureF = m_depthOfFieldSettings->GetApertureF();
+            if (ImGui::SliderFloat("Aperture F", &apertureF, 0.0f, 1.0f, "%0.4f") || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetApertureF(apertureF);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+
+            constexpr float Min = DepthOfField::ApertureFMin;
+            constexpr float Max = DepthOfField::ApertureFMax;
+            float viewApertureF = 1.0f / Max + (1.0f / Min - 1.0f / Max) * apertureF;
+            viewApertureF = 1.0f / viewApertureF;
+            ImGui::Text("f / %f", viewApertureF);
+            ImGui::Spacing();
+
+            float viewNear = 1.f;
+            float viewFar = 100.f;
+            Camera::CameraRequestBus::EventResult(viewNear, GetCameraEntityId(), &Camera::CameraRequestBus::Events::GetNearClipDistance);
+            Camera::CameraRequestBus::EventResult(viewFar, GetCameraEntityId(), &Camera::CameraRequestBus::Events::GetFarClipDistance);
+            
+            float focusDistance = m_depthOfFieldSettings->GetFocusDistance();
+            if (ImGui::SliderFloat("Focus Distance", &focusDistance, ViewNear, ViewFar, "%0.3f") || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetFocusDistance(focusDistance);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+            
+            bool dofEnabled = m_depthOfFieldSettings->GetEnabled();
+            if (ImGui::Checkbox("Enabled Depth of Field", &dofEnabled) || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetEnabled(dofEnabled);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+            
+            bool enabledDebugColoring = m_depthOfFieldSettings->GetEnableDebugColoring();
+            if (ImGui::Checkbox("Enabled Debug Color", &enabledDebugColoring) || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetEnableDebugColoring(enabledDebugColoring);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+            ImGui::Spacing();
+            ImGui::Spacing();
+
+            ImGui::Text("Auto Focus");
+            ImGui::Spacing();
+            
+            bool enabledAutoFocus = m_depthOfFieldSettings->GetEnableAutoFocus();
+            if (ImGui::Checkbox("Enabled Auto Focus", &enabledAutoFocus) || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetEnableAutoFocus(enabledAutoFocus);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+
+            float screenPosition[2];
+            AZ::Vector2 autoFocusScreenPosition = m_depthOfFieldSettings->GetAutoFocusScreenPosition();
+            screenPosition[0] = autoFocusScreenPosition.GetX();
+            screenPosition[1] = autoFocusScreenPosition.GetY();
+            if (ImGui::SliderFloat2("Screen Position", screenPosition, 0.0f, 1.0f, "%0.3f") || !m_isInitParameters)
+            {
+                autoFocusScreenPosition.SetX(screenPosition[0]);
+                autoFocusScreenPosition.SetY(screenPosition[1]);
+                m_depthOfFieldSettings->SetAutoFocusScreenPosition(autoFocusScreenPosition);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+            
+            float autoFocusSensitivity = m_depthOfFieldSettings->GetAutoFocusSensitivity();
+            if (ImGui::SliderFloat("Sensitivity", &autoFocusSensitivity, 0.0f, 1.0f , "%0.3f") || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetAutoFocusSensitivity(autoFocusSensitivity);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+            
+            float autoFocusSpeed = m_depthOfFieldSettings->GetAutoFocusSpeed();
+            if (ImGui::SliderFloat("Speed", &autoFocusSpeed, 0.0f, DepthOfField::AutoFocusSpeedMax, "%0.3f") || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetAutoFocusSpeed(autoFocusSpeed);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+            
+            float autoFocusDelay = m_depthOfFieldSettings->GetAutoFocusDelay();
+            if (ImGui::SliderFloat("Delay", &autoFocusDelay, 0.0f, DepthOfField::AutoFocusDelayMax, "%0.3f") || !m_isInitParameters)
+            {
+                m_depthOfFieldSettings->SetAutoFocusDelay(autoFocusDelay);
+                m_depthOfFieldSettings->OnConfigChanged();
+            }
+
+            m_isInitParameters = true;
+        }
+
+        ImGui::Unindent();
+
+        ImGui::Separator();
+
+        m_imguiSidebar.End();
+    }
+
+} // namespace AtomSampleViewer

+ 105 - 0
Gem/Code/Source/DepthOfFieldExampleComponent.h

@@ -0,0 +1,105 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <Atom/Feature/PostProcess/PostProcessFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <Utils/ImGuiSidebar.h>
+
+namespace AtomSampleViewer
+{
+    /*
+    * This component creates a simple scene to Depth of Field.
+    */
+    class DepthOfFieldExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(DepthOfFieldExampleComponent, "{F2CE0FCC-F3CE-4900-929D-74DCF554DF97}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        DepthOfFieldExampleComponent() = default;
+        ~DepthOfFieldExampleComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time);
+
+        // AZ::EntityBus::MultiHandler...
+        void OnEntityDestruction(const AZ::EntityId& entityId) override;
+
+        void UseArcBallCameraController();
+        void RemoveController();
+
+        void CreateMeshes();
+        void CreateLight();
+        void CreateDepthOfFieldEntity();
+
+        void DrawSidebar();
+
+        static constexpr uint32_t BunnyNumber = 32;
+        static constexpr float DistanceBetweenUnits = 1.0f;
+        static constexpr float ModelScaleRatio = DistanceBetweenUnits * 1.3f;
+        static constexpr float ViewNear = 0.2f;
+        static constexpr float ViewFar = ViewNear + DistanceBetweenUnits * BunnyNumber;
+        static constexpr float FocusDefault = ViewNear + (ViewFar - ViewNear) * 0.35f;
+        static constexpr float ViewFovDegrees = 20.0f;
+
+        // owning entity
+        AZ::Entity* m_depthOfFieldEntity = nullptr;
+        
+        // asset
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_bunnyModelAsset;
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_materialAsset;
+
+        // Mesh handles
+        using MeshHandle = AZ::Render::MeshFeatureProcessorInterface::MeshHandle;
+        AZStd::array<MeshHandle, BunnyNumber> m_meshHandles;
+
+        // Light handle
+        using DirectionalLightHandle = AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle;
+        DirectionalLightHandle m_directionalLightHandle;
+
+        // GUI
+        ImGuiSidebar m_imguiSidebar;
+
+        // initialize flag
+        bool m_isInitCamera = false;
+        bool m_isInitParameters = false;
+
+        // save default camera parameter on base viewer.
+        // When deactivate this component, return the parameters to camera.
+        float m_saveDefaultNearOnAtomSampleViewer = 0.1f;
+        float m_saveDefaultFarOnAtomSampleViewer = 10.0f;
+        float m_saveDefaultFovDegreesOnAtomSampleViewer = 20.0f;
+
+        // feature processors
+        AZ::Render::PostProcessFeatureProcessorInterface* m_postProcessFeatureProcessor = nullptr;
+        AZ::Render::DirectionalLightFeatureProcessorInterface* m_directionalLightFeatureProcessor = nullptr;
+
+        AZ::Render::DepthOfFieldSettingsInterface* m_depthOfFieldSettings = nullptr;
+        AZ::Render::PostProcessSettingsInterface* m_postProcessSettings = nullptr;
+    };
+} // namespace AtomSampleViewer

+ 718 - 0
Gem/Code/Source/DiffuseGIExampleComponent.cpp

@@ -0,0 +1,718 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <DiffuseGIExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+#include <Atom/Feature/CoreLights/PhotometricValue.h>
+#include <Atom/RHI/Device.h>
+#include <Atom/RHI/Factory.h>
+
+#include <Atom/RPI.Public/View.h>
+
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <AzCore/Asset/AssetManagerBus.h>
+#include <AzCore/Component/Entity.h>
+#include <AzCore/IO/IOUtils.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/std/smart_ptr/make_shared.h>
+#include <AzCore/std/sort.h>
+
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <EntityUtilityFunctions.h>
+
+#include <Automation/ScriptableImGui.h>
+
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Scene.h>
+
+namespace AtomSampleViewer
+{
+    const char* DiffuseGIExampleComponent::CornellBoxColorNames[] =
+    {
+        "Red",
+        "Green",
+        "Blue",
+        "Yellow",
+        "White"
+    };
+
+    const AZ::Render::ShadowmapSize DiffuseGIExampleComponent::s_shadowmapSizes[] =
+    {
+        AZ::Render::ShadowmapSize::Size256,
+        AZ::Render::ShadowmapSize::Size512,
+        AZ::Render::ShadowmapSize::Size1024,
+        AZ::Render::ShadowmapSize::Size2048
+    };
+
+    void DiffuseGIExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<DiffuseGIExampleComponent, Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    void DiffuseGIExampleComponent::Activate()
+    {
+        m_imguiSidebar.Activate();
+
+        AZ::TickBus::Handler::BusConnect();
+
+        // setup the camera
+        Camera::CameraRequestBus::EventResult(m_originalFarClipDistance, GetCameraEntityId(), &Camera::CameraRequestBus::Events::GetFarClipDistance);
+        Camera::CameraRequestBus::Event(GetCameraEntityId(), &Camera::CameraRequestBus::Events::SetFarClipDistance, 180.f);
+
+        // disable global Ibl in the example scene since we're controlling it separately
+        DisableGlobalIbl();
+
+        LoadSampleSceneAssets();
+    }
+
+    void DiffuseGIExampleComponent::UnloadSampleScene(bool geometryOnly)
+    {
+        AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+
+        // release meshes
+        AZ::Render::MeshFeatureProcessorInterface* meshFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::MeshFeatureProcessorInterface>();
+        for (auto& meshHandle : m_meshHandles)
+        {
+            meshFeatureProcessor->ReleaseMesh(meshHandle);
+        }
+
+        // unload everything else if necessary
+        if (!geometryOnly)
+        {
+            if (m_diffuseProbeGrid)
+            {
+                AZ::Render::DiffuseProbeGridFeatureProcessorInterface* diffuseProbeGridFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DiffuseProbeGridFeatureProcessorInterface>();
+                diffuseProbeGridFeatureProcessor->RemoveProbeGrid(m_diffuseProbeGrid);
+            }
+
+            if (m_directionalLightHandle.IsValid())
+            {
+                AZ::Render::DirectionalLightFeatureProcessorInterface* directionalLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DirectionalLightFeatureProcessorInterface>();
+                directionalLightFeatureProcessor->ReleaseLight(m_directionalLightHandle);
+            }
+
+            if (m_pointLightHandle.IsValid())
+            {
+                AZ::Render::PointLightFeatureProcessorInterface* pointLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PointLightFeatureProcessorInterface>();
+                pointLightFeatureProcessor->ReleaseLight(m_pointLightHandle);
+            }
+        }
+    }
+
+    void DiffuseGIExampleComponent::SetSampleScene(bool geometryOnly /*= false*/)
+    {
+        UnloadSampleScene(geometryOnly);
+
+        // set new scene
+        switch (m_sampleScene)
+        {
+        case SampleScene::CornellBox:
+            CreateCornellBoxScene(geometryOnly);
+            break;
+        case SampleScene::Bistro:
+            CreateBistroScene();
+            break;
+        };
+    }
+
+    void DiffuseGIExampleComponent::Deactivate()
+    {
+        // disable camera
+        Camera::CameraRequestBus::Event(GetCameraEntityId(), &Camera::CameraRequestBus::Events::SetFarClipDistance, m_originalFarClipDistance);
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+
+        AZ::TickBus::Handler::BusDisconnect();
+        m_imguiSidebar.Deactivate();
+        m_defaultIbl.Reset();
+
+        UnloadSampleScene(false);
+    }
+
+    float DiffuseGIExampleComponent::ComputePointLightAttenuationRadius(AZ::Color color, float intensity)
+    {
+        static const float CutoffIntensity = 0.1f;
+
+        float luminance = AZ::Render::PhotometricValue::GetPerceptualLuminance(color * intensity);
+        return sqrt(luminance / CutoffIntensity);
+    }
+
+    void DiffuseGIExampleComponent::LoadSampleSceneAssets()
+    {
+        // load plane and cube models
+        // all geometry in the CornellBox is created from planes and boxes
+        static constexpr const char PlaneModelPath[] = "objects/plane.azmodel";
+        AZ::Data::AssetId planeAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(PlaneModelPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_planeModelAsset.Create(planeAssetId);
+
+        static constexpr const char CubeModelPath[] = "objects/cube.azmodel";
+        AZ::Data::AssetId cubeAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(CubeModelPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_cubeModelAsset.Create(cubeAssetId);
+
+        // materials for the CornellBox
+        static constexpr const char RedMaterialPath[] = "materials/diffusegiexample/red.azmaterial";
+        AZ::Data::AssetId redMaterialAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(RedMaterialPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_redMaterialAsset.Create(redMaterialAssetId);
+
+        static constexpr const char GreenMaterialPath[] = "materials/diffusegiexample/green.azmaterial";
+        AZ::Data::AssetId greenMaterialAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(GreenMaterialPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_greenMaterialAsset.Create(greenMaterialAssetId);
+
+        static constexpr const char BlueMaterialPath[] = "materials/diffusegiexample/blue.azmaterial";
+        AZ::Data::AssetId blueMaterialAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(BlueMaterialPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_blueMaterialAsset.Create(blueMaterialAssetId);
+
+        static constexpr const char YellowMaterialPath[] = "materials/diffusegiexample/yellow.azmaterial";
+        AZ::Data::AssetId yellowMaterialAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(YellowMaterialPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_yellowMaterialAsset.Create(yellowMaterialAssetId);
+
+        static constexpr const char WhiteMaterialPath[] = "materials/diffusegiexample/white.azmaterial";
+        AZ::Data::AssetId whiteMaterialAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(WhiteMaterialPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_whiteMaterialAsset.Create(whiteMaterialAssetId);
+
+        // Bistro models
+        static constexpr const char InteriorModelPath[] = "objects/bistro/Bistro_Research_Interior.azmodel";
+        AZ::Data::AssetId interiorAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(InteriorModelPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_bistroInteriorModelAsset.Create(interiorAssetId);
+
+        static constexpr const char ExteriorModelPath[] = "objects/bistro/Bistro_Research_Exterior.azmodel";
+        AZ::Data::AssetId exteriorAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(ExteriorModelPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+        m_bistroExteriorModelAsset.Create(exteriorAssetId);
+
+        // diffuse IBL cubemap
+        const constexpr char* DiffuseAssetPath = "lightingpresets/lowcontrast/palermo_sidewalk_4k_iblskyboxcm_ibldiffuse.exr.streamingimage";
+        if (!m_diffuseImageAsset.IsReady())
+        {
+            m_diffuseImageAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::StreamingImageAsset>(DiffuseAssetPath, AZ::RPI::AssetUtils::TraceLevel::Assert);
+            m_diffuseImageAsset.QueueLoad();
+        }
+    }
+
+    AZ::Data::Asset<AZ::RPI::MaterialAsset>& DiffuseGIExampleComponent::GetCornellBoxMaterialAsset(CornellBoxColors color)
+    {
+        switch (color)
+        {
+        case CornellBoxColors::Red:
+            return m_redMaterialAsset;
+        case CornellBoxColors::Green:
+            return m_greenMaterialAsset;
+        case CornellBoxColors::Blue:
+            return m_blueMaterialAsset;
+        case CornellBoxColors::Yellow:
+            return m_yellowMaterialAsset;
+        case CornellBoxColors::White:
+            return m_whiteMaterialAsset;
+        };
+
+        AZ_Assert(false, "Unknown material color");
+        return m_whiteMaterialAsset;
+    }
+
+    void DiffuseGIExampleComponent::CreateCornellBoxScene(bool geometryOnly)
+    {
+        m_meshHandles.resize(aznumeric_cast<uint32_t>(CornellBoxMeshes::Count));
+        
+        // left wall
+        if (m_leftWallVisible)
+        {
+            AZ::Render::MaterialAssignmentMap materialMap;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = GetCornellBoxMaterialAsset(m_leftWallColor);
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = AZ::RPI::Material::FindOrCreate(GetCornellBoxMaterialAsset(m_leftWallColor));
+        
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            transform.SetTranslation(-5.0f, 0.0f, 0.0f);
+            transform *= AZ::Transform::CreateRotationY(AZ::Constants::HalfPi);
+            transform.MultiplyByScale(AZ::Vector3(10.05f, 10.05f, 1.0f));
+            m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::LeftWall)] = GetMeshFeatureProcessor()->AcquireMesh(m_planeModelAsset, materialMap);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::LeftWall)], transform);
+        }
+        
+        // right wall
+        if (m_rightWallVisible)
+        {
+            AZ::Render::MaterialAssignmentMap materialMap;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = GetCornellBoxMaterialAsset(m_rightWallColor);
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = AZ::RPI::Material::FindOrCreate(GetCornellBoxMaterialAsset(m_rightWallColor));
+        
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            transform.SetTranslation(5.0f, 0.0f, 0.0f);
+            transform *= AZ::Transform::CreateRotationY(-AZ::Constants::HalfPi);
+            transform.MultiplyByScale(AZ::Vector3(10.05f, 10.05f, 1.0f));
+            m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::RightWall)] = GetMeshFeatureProcessor()->AcquireMesh(m_planeModelAsset, materialMap);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::RightWall)], transform);
+        }
+        
+        // back wall
+        if (m_backWallVisible)
+        {
+            AZ::Render::MaterialAssignmentMap materialMap;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = GetCornellBoxMaterialAsset(m_backWallColor);
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = AZ::RPI::Material::FindOrCreate(GetCornellBoxMaterialAsset(m_backWallColor));
+        
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            transform.SetTranslation(0.0f, 5.0f, 0.0f);
+            transform *= AZ::Transform::CreateRotationX(AZ::Constants::HalfPi);
+            transform.MultiplyByScale(AZ::Vector3(10.05f, 10.05f, 1.0f));
+            m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::BackWall)] = GetMeshFeatureProcessor()->AcquireMesh(m_planeModelAsset, materialMap);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::BackWall)], transform);
+        }
+        
+        // ceiling
+        if (m_ceilingVisible)
+        {
+            AZ::Render::MaterialAssignmentMap materialMap;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = GetCornellBoxMaterialAsset(m_ceilingColor);
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = AZ::RPI::Material::FindOrCreate(GetCornellBoxMaterialAsset(m_ceilingColor));
+        
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            transform.SetTranslation(0.0f, 0.0f, 5.0f);
+            transform *= AZ::Transform::CreateRotationX(AZ::Constants::Pi);
+            transform.MultiplyByScale(AZ::Vector3(10.05f, 10.05f, 1.0f));
+            m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::Ceiling)] = GetMeshFeatureProcessor()->AcquireMesh(m_planeModelAsset, materialMap);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::Ceiling)], transform);
+        }
+        
+        // floor
+        if (m_floorVisible)
+        {
+            AZ::Render::MaterialAssignmentMap materialMap;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = GetCornellBoxMaterialAsset(m_floorColor);
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = AZ::RPI::Material::FindOrCreate(GetCornellBoxMaterialAsset(m_floorColor));
+        
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            transform.SetTranslation(0.0f, 0.0f, -5.0f);
+            transform.MultiplyByScale(AZ::Vector3(10.05f, 10.05f, 1.0f));
+            m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::Floor)] = GetMeshFeatureProcessor()->AcquireMesh(m_planeModelAsset, materialMap);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::Floor)], transform);
+        }
+        
+        // large box
+        {
+            AZ::Render::MaterialAssignmentMap materialMap;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = m_whiteMaterialAsset;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = AZ::RPI::Material::FindOrCreate(m_whiteMaterialAsset);
+        
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            transform.SetTranslation(-2.0f, 0.0f, -2.0f);
+            transform *= AZ::Transform::CreateRotationZ(AZ::Constants::HalfPi * 0.2f);
+            transform.MultiplyByScale(AZ::Vector3(3.0f, 3.0f, 6.0f));
+            m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::LargeBox)] = GetMeshFeatureProcessor()->AcquireMesh(m_cubeModelAsset, materialMap);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::LargeBox)], transform);
+        }
+        
+        // small box
+        {
+            AZ::Render::MaterialAssignmentMap materialMap;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = m_whiteMaterialAsset;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = AZ::RPI::Material::FindOrCreate(m_whiteMaterialAsset);
+        
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+            transform.SetTranslation(2.0f, -1.5f, -3.5f);
+            transform *= AZ::Transform::CreateRotationZ(-AZ::Constants::HalfPi * 0.2f);
+            transform.MultiplyByScale(AZ::Vector3(3.0f, 3.0f, 3.0f));
+            m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::SmallBox)] = GetMeshFeatureProcessor()->AcquireMesh(m_cubeModelAsset, materialMap);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(CornellBoxMeshes::SmallBox)], transform);
+        }
+
+        // stop now if we were only loading the geometry
+        if (geometryOnly)
+        {
+            return;
+        }
+
+        m_pointLightPos = AZ::Vector3(0.0f, -1.0f, 4.0f);
+        m_pointLightColor = AZ::Color(1.0f, 1.0f, 1.0f, 1.0f);
+        m_pointLightIntensity = 20.0f;
+
+        // point light
+        {
+            AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+            AZ::Render::PointLightFeatureProcessorInterface* pointLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PointLightFeatureProcessorInterface>();
+            m_pointLightHandle = pointLightFeatureProcessor->AcquireLight();
+            pointLightFeatureProcessor->SetPosition(m_pointLightHandle, m_pointLightPos);
+            pointLightFeatureProcessor->SetRgbIntensity(m_pointLightHandle, AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Candela>(m_pointLightIntensity * m_pointLightColor));
+            pointLightFeatureProcessor->SetBulbRadius(m_pointLightHandle, 1.0f);
+            pointLightFeatureProcessor->SetAttenuationRadius(m_pointLightHandle, ComputePointLightAttenuationRadius(m_pointLightColor, m_pointLightIntensity));
+        }
+
+        // diffuse probe grid
+        {
+            AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+            AZ::Render::DiffuseProbeGridFeatureProcessorInterface* diffuseProbeGridFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DiffuseProbeGridFeatureProcessorInterface>();
+            AZ::Transform transform = AZ::Transform::CreateIdentity();
+
+            m_origin.Set(0.3f, -0.25f, 0.5f);
+            transform.SetTranslation(m_origin);
+            m_diffuseProbeGrid = diffuseProbeGridFeatureProcessor->AddProbeGrid(transform, AZ::Vector3(12.0f, 12.0f, 12.0f), AZ::Vector3(1.5f, 1.5f, 2.0f));
+            diffuseProbeGridFeatureProcessor->SetAmbientMultiplier(m_diffuseProbeGrid, m_ambientMultiplier);
+
+            m_viewBias = 0.7f;
+            diffuseProbeGridFeatureProcessor->SetViewBias(m_diffuseProbeGrid, m_viewBias);
+
+            m_normalBias = 0.1f;
+            diffuseProbeGridFeatureProcessor->SetNormalBias(m_diffuseProbeGrid, m_normalBias);
+        }
+
+        // camera
+        m_cameraTranslation = AZ::Vector3(0.0f, -17.5f, 0.0f);
+
+        // disable diffuse Ibl
+        DisableGlobalIbl();
+    }
+
+    void DiffuseGIExampleComponent::CreateBistroScene()
+    {
+        m_meshHandles.resize(aznumeric_cast<uint32_t>(BistroMeshes::Count));
+
+        AZ::Transform transform = AZ::Transform::CreateIdentity();
+        m_meshHandles[aznumeric_cast<uint32_t>(BistroMeshes::Inside)] = GetMeshFeatureProcessor()->AcquireMesh(m_bistroInteriorModelAsset);
+        GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(BistroMeshes::Inside)], transform);
+        
+        m_meshHandles[aznumeric_cast<uint32_t>(BistroMeshes::Outside)] = GetMeshFeatureProcessor()->AcquireMesh(m_bistroExteriorModelAsset);
+        GetMeshFeatureProcessor()->SetTransform(m_meshHandles[aznumeric_cast<uint32_t>(BistroMeshes::Outside)], transform);
+        
+        m_directionalLightPitch = AZ::DegToRad(-45.0f);
+        m_directionalLightYaw = AZ::DegToRad(180.0f);
+        m_directionalLightColor = AZ::Color(0.92f, 0.78f, 0.35f, 1.0f);
+        m_directionalLightIntensity = 20.0f;
+
+        m_pointLightPos = AZ::Vector3(-3.75f, -4.5f, 1.5f);
+        m_pointLightColor = AZ::Color(1.0f, 1.0f, 1.0f, 1.0f);
+        m_pointLightIntensity = 20.0f;
+
+        m_ambientMultiplier = 1.0f;
+
+        // directional light
+        {
+            AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+            AZ::Render::DirectionalLightFeatureProcessorInterface* directionalLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DirectionalLightFeatureProcessorInterface>();
+            m_directionalLightHandle = directionalLightFeatureProcessor->AcquireLight();
+            const auto lightTransform = AZ::Transform::CreateRotationZ(m_directionalLightYaw) * AZ::Transform::CreateRotationX(m_directionalLightPitch);
+            directionalLightFeatureProcessor->SetDirection(m_directionalLightHandle, lightTransform.GetBasis(1));
+            directionalLightFeatureProcessor->SetRgbIntensity(m_directionalLightHandle, AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Lux>(m_directionalLightIntensity * m_directionalLightColor));
+            directionalLightFeatureProcessor->SetCascadeCount(m_directionalLightHandle, 4);
+            directionalLightFeatureProcessor->SetShadowmapSize(m_directionalLightHandle, AZ::Render::ShadowmapSize::Size2048);
+            directionalLightFeatureProcessor->SetViewFrustumCorrectionEnabled(m_directionalLightHandle, false);
+            directionalLightFeatureProcessor->SetShadowFilterMethod(m_directionalLightHandle, AZ::Render::ShadowFilterMethod::EsmPcf);
+            directionalLightFeatureProcessor->SetShadowBoundaryWidth(m_directionalLightHandle, 0.03f);
+            directionalLightFeatureProcessor->SetPredictionSampleCount(m_directionalLightHandle, 4);
+            directionalLightFeatureProcessor->SetFilteringSampleCount(m_directionalLightHandle, 16);
+            directionalLightFeatureProcessor->SetGroundHeight(m_directionalLightHandle, 0.f);
+        }
+
+        // point light
+        {
+            AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+            AZ::Render::PointLightFeatureProcessorInterface* pointLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PointLightFeatureProcessorInterface>();
+            m_pointLightHandle = pointLightFeatureProcessor->AcquireLight();
+            pointLightFeatureProcessor->SetPosition(m_pointLightHandle, m_pointLightPos);
+            pointLightFeatureProcessor->SetRgbIntensity(m_pointLightHandle, AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Candela>(m_pointLightIntensity * m_pointLightColor));
+            pointLightFeatureProcessor->SetBulbRadius(m_pointLightHandle, 1.0f);
+            pointLightFeatureProcessor->SetAttenuationRadius(m_pointLightHandle, ComputePointLightAttenuationRadius(m_pointLightColor, m_pointLightIntensity));
+        }
+
+        // diffuse probe grid
+        {
+            AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+            AZ::Render::DiffuseProbeGridFeatureProcessorInterface* diffuseProbeGridFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DiffuseProbeGridFeatureProcessorInterface>();
+            transform = AZ::Transform::CreateIdentity();
+        
+            m_origin.Set(1.4f, -1.25f, 5.0f);
+            transform.SetTranslation(m_origin);
+            m_diffuseProbeGrid = diffuseProbeGridFeatureProcessor->AddProbeGrid(transform, AZ::Vector3(165.0f, 95.0f, 45.0f), AZ::Vector3(4.0f, 4.0f, 5.0f));
+            diffuseProbeGridFeatureProcessor->SetAmbientMultiplier(m_diffuseProbeGrid, m_ambientMultiplier);
+
+            m_viewBias = 0.55f;
+            diffuseProbeGridFeatureProcessor->SetViewBias(m_diffuseProbeGrid, m_viewBias);
+
+            m_normalBias = 0.4f;
+            diffuseProbeGridFeatureProcessor->SetNormalBias(m_diffuseProbeGrid, m_normalBias);
+        }
+
+        // camera
+        m_cameraTranslation = AZ::Vector3(2.0f, -17.0f, 10.0f);
+
+        // diffuse Ibl
+        EnableDiffuseIbl();
+    }
+
+    void DiffuseGIExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
+    {
+        if (m_resetCamera)
+        {
+            AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Reset);
+            AZ::TransformBus::Event(GetCameraEntityId(), &AZ::TransformBus::Events::SetWorldTranslation, m_cameraTranslation);
+            AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable, azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+            m_resetCamera = false;
+        }
+
+        // ImGui sidebar
+        AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        AZ::Render::DiffuseProbeGridFeatureProcessorInterface* diffuseProbeGridFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DiffuseProbeGridFeatureProcessorInterface>();
+        AZ::Render::PointLightFeatureProcessorInterface* pointLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PointLightFeatureProcessorInterface>();
+        AZ::Render::DirectionalLightFeatureProcessorInterface* directionalLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DirectionalLightFeatureProcessorInterface>();
+
+        bool sceneChanged = false;
+
+        // initialize the scene in OnTick() in order to delay a frame between samples
+        if (m_sampleScene == SampleScene::None)
+        {
+            m_sampleScene = CornellBox;
+            sceneChanged = true;
+        }
+
+        if (m_imguiSidebar.Begin())
+        {
+            // sample scene
+            {
+                ImGui::Text("Scene");
+                sceneChanged |= ImGui::RadioButton("Cornell Box", (int*)&m_sampleScene, aznumeric_cast<uint32_t>(CornellBox));
+                sceneChanged |= ImGui::RadioButton("Bistro", (int*)&m_sampleScene, aznumeric_cast<uint32_t>(Bistro));
+                ImGui::NewLine();
+            }
+
+            // diffuse probe grid settings
+            {
+                bool diffuseProbeGridChanged = false;
+                diffuseProbeGridChanged |= ImGui::Checkbox("Enable Diffuse GI", &m_enableDiffuseGI);
+                diffuseProbeGridChanged |= ImGui::SliderFloat("##AmbientMultiplier", &m_ambientMultiplier, 0.0f, 10.0f);
+                ImGui::NewLine();
+
+                ImGui::Text("View Bias");
+                diffuseProbeGridChanged |= ImGui::SliderFloat("##ViewBias", &m_viewBias, 0.01f, 2.0f);
+
+                ImGui::Text("Normal Bias");
+                diffuseProbeGridChanged |= ImGui::SliderFloat("##NormalBias", &m_normalBias, 0.01f, 2.0f);
+                diffuseProbeGridChanged |= ImGui::Checkbox("GI Shadows", &m_giShadows);
+
+                ImGui::NewLine();
+
+                ImGui::Text("Grid Origin");
+                float originX = m_origin.GetX();
+                diffuseProbeGridChanged |= ImGui::SliderFloat("##OriginX", &originX, -10.0f, 10.0f);
+                float originY = m_origin.GetY();
+                diffuseProbeGridChanged |= ImGui::SliderFloat("##OriginY", &originY, -10.0f, 10.0f);
+                float originZ = m_origin.GetZ();
+                diffuseProbeGridChanged |= ImGui::SliderFloat("##OriginZ", &originZ, -10.0f, 10.0f);
+
+                m_origin.SetX(originX);
+                m_origin.SetY(originY);
+                m_origin.SetZ(originZ);
+
+                if (diffuseProbeGridChanged && diffuseProbeGridFeatureProcessor->IsValidProbeGridHandle(m_diffuseProbeGrid))
+                {
+                    diffuseProbeGridFeatureProcessor->Enable(m_diffuseProbeGrid, m_enableDiffuseGI);
+                    diffuseProbeGridFeatureProcessor->SetAmbientMultiplier(m_diffuseProbeGrid, m_ambientMultiplier);
+                    diffuseProbeGridFeatureProcessor->SetViewBias(m_diffuseProbeGrid, m_viewBias);
+                    diffuseProbeGridFeatureProcessor->SetNormalBias(m_diffuseProbeGrid, m_normalBias);
+                    diffuseProbeGridFeatureProcessor->SetGIShadows(m_diffuseProbeGrid, m_giShadows);
+
+                    AZ::Transform transform = AZ::Transform::CreateIdentity();
+                    transform.SetTranslation(m_origin);
+                    diffuseProbeGridFeatureProcessor->SetTransform(m_diffuseProbeGrid, transform);
+                }
+
+                ImGui::NewLine();
+            }
+
+            // diffuse IBL (Bistro only)
+            if (m_sampleScene == SampleScene::Bistro)
+            {
+                bool diffuseIblChanged = false;
+                diffuseIblChanged |= ImGui::Checkbox("Diffuse IBL (Sky Light)", &m_useDiffuseIbl);
+                diffuseIblChanged |= ImGui::SliderFloat("Exposure", &m_diffuseIblExposure, -10.0f, 10.0f);
+
+                if (diffuseIblChanged && diffuseProbeGridFeatureProcessor->IsValidProbeGridHandle(m_diffuseProbeGrid))
+                {
+                    diffuseProbeGridFeatureProcessor->SetUseDiffuseIbl(m_diffuseProbeGrid, m_useDiffuseIbl);
+
+                    if (m_useDiffuseIbl)
+                    {
+                        EnableDiffuseIbl();
+                    }
+                    else
+                    {
+                        DisableGlobalIbl();
+                    }
+                }
+
+                ImGui::NewLine();
+            }
+
+            // directional light
+            if (m_directionalLightHandle.IsValid())
+            {
+                bool directionalLightChanged = false;
+                ImGui::Text("Directional Light");
+                directionalLightChanged |= ImGui::SliderAngle("Pitch", &m_directionalLightPitch, -90.0f, 0.f);
+                directionalLightChanged |= ImGui::SliderAngle("Yaw", &m_directionalLightYaw, 0.f, 360.f);
+
+                ImGui::Text("Color");
+                float directionalLightColor[4];
+                m_directionalLightColor.StoreToFloat4(&directionalLightColor[0]);
+                directionalLightChanged |= ImGui::ColorEdit3("", &directionalLightColor[0]);
+
+                ImGui::Text("Intensity");
+                directionalLightChanged |= ImGui::SliderFloat("##DirectionalLightIntensity", &m_directionalLightIntensity, 0, 100);
+
+                if (directionalLightChanged)
+                {
+                    const auto lightTransform = AZ::Transform::CreateRotationZ(m_directionalLightYaw) * AZ::Transform::CreateRotationX(m_directionalLightPitch);
+                    directionalLightFeatureProcessor->SetDirection(m_directionalLightHandle, lightTransform.GetBasis(1));
+
+                    m_directionalLightColor = AZ::Color(directionalLightColor[0], directionalLightColor[1], directionalLightColor[2], 1.0f);
+                    directionalLightFeatureProcessor->SetRgbIntensity(m_directionalLightHandle, AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Lux>(m_directionalLightColor * m_directionalLightIntensity));
+                }
+
+                // Camera Configuration
+                {
+                    Camera::Configuration config;
+                    Camera::CameraRequestBus::EventResult(
+                        config,
+                        GetCameraEntityId(),
+                        &Camera::CameraRequestBus::Events::GetCameraConfiguration);
+                    directionalLightFeatureProcessor->SetCameraConfiguration(
+                        m_directionalLightHandle,
+                        config);
+                }
+
+                // Camera Transform
+                {
+                    AZ::Transform transform = AZ::Transform::CreateIdentity();
+                    AZ::TransformBus::EventResult(
+                        transform,
+                        GetCameraEntityId(),
+                        &AZ::TransformBus::Events::GetWorldTM);
+                    directionalLightFeatureProcessor->SetCameraTransform(
+                        m_directionalLightHandle, transform);
+                }
+
+                ImGui::NewLine();
+            }
+
+            // point light
+            if (m_pointLightHandle.IsValid())
+            {
+                bool pointLightChanged = false;
+                ImGui::Text("Point Light");
+                float lightX = m_pointLightPos.GetX();
+                pointLightChanged |= ImGui::SliderFloat("##PointLightX", &lightX, -10.0f, 10.0f);
+                float lightY = m_pointLightPos.GetY();
+                pointLightChanged |= ImGui::SliderFloat("##PointLightY", &lightY, -10.0f, 10.0f);
+                float lightZ = m_pointLightPos.GetZ();
+                pointLightChanged |= ImGui::SliderFloat("##PointLightZ", &lightZ, -10.0f, 10.0f);
+
+                m_pointLightPos.SetX(lightX);
+                m_pointLightPos.SetY(lightY);
+                m_pointLightPos.SetZ(lightZ);
+
+                float pointLightColor[4];
+                m_pointLightColor.StoreToFloat4(&pointLightColor[0]);
+                pointLightChanged |= ImGui::ColorEdit3("Point Light Color", &pointLightColor[0]);
+
+                ImGui::Text("Intensity");
+                pointLightChanged |= ImGui::SliderFloat("##PointLightIntensity", &m_pointLightIntensity, 0, 100);
+
+                if (pointLightChanged)
+                {
+                    m_pointLightColor = AZ::Color(pointLightColor[0], pointLightColor[1], pointLightColor[2], 1.0f);
+
+                    pointLightFeatureProcessor->SetPosition(m_pointLightHandle, m_pointLightPos);
+                    pointLightFeatureProcessor->SetRgbIntensity(m_pointLightHandle, AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Candela>(m_pointLightColor * m_pointLightIntensity));
+                    pointLightFeatureProcessor->SetAttenuationRadius(m_pointLightHandle, ComputePointLightAttenuationRadius(m_pointLightColor, m_pointLightIntensity));
+                }
+
+                ImGui::NewLine();
+            }
+
+            // Cornell Box specific
+            if (m_sampleScene == CornellBox)
+            {
+                bool geometryChanged = false;
+                geometryChanged |= ImGui::Checkbox("Left Wall Visible", &m_leftWallVisible);
+                geometryChanged |= ImGui::Combo("Left Wall Color", (int*)&m_leftWallColor, CornellBoxColorNames, CornellBoxColorNamesCount);
+                geometryChanged |= ImGui::Checkbox("Right Wall Visible", &m_rightWallVisible);
+                geometryChanged |= ImGui::Combo("Right Wall Color", (int*)&m_rightWallColor, CornellBoxColorNames, CornellBoxColorNamesCount);
+                geometryChanged |= ImGui::Checkbox("Back Wall Visible", &m_backWallVisible);
+                geometryChanged |= ImGui::Combo("Back Wall Color", (int*)&m_backWallColor, CornellBoxColorNames, CornellBoxColorNamesCount);
+                geometryChanged |= ImGui::Checkbox("Floor Visible", &m_floorVisible);
+                geometryChanged |= ImGui::Combo("Floor Color", (int*)&m_floorColor, CornellBoxColorNames, CornellBoxColorNamesCount);
+                geometryChanged |= ImGui::Checkbox("Ceiling Visible", &m_ceilingVisible);
+                geometryChanged |= ImGui::Combo("Ceiling Color", (int*)&m_ceilingColor, CornellBoxColorNames, CornellBoxColorNamesCount);
+
+                if (geometryChanged)
+                {
+                    SetSampleScene(true);
+                }
+            }
+
+            m_imguiSidebar.End();
+        }
+
+        if (sceneChanged)
+        {
+            m_resetCamera = true;
+            SetSampleScene();
+        }
+    }
+
+    void DiffuseGIExampleComponent::OnEntityDestruction(const AZ::EntityId& entityId)
+    {
+        AZ::EntityBus::MultiHandler::BusDisconnect(entityId);
+    }
+
+    void DiffuseGIExampleComponent::DisableGlobalIbl()
+    {
+        AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+
+        // disable Ibl by setting the empty cubemap
+        const constexpr char* DiffuseAssetPath = "textures/default/default_iblglobalcm_ibldiffuse.dds.streamingimage";
+        const constexpr char* SpecularAssetPath = "textures/default/default_iblglobalcm_iblspecular.dds.streamingimage";
+
+        auto assertTraceLevel = AZ::RPI::AssetUtils::TraceLevel::Assert;
+        AZ::Data::Asset<AZ::RPI::StreamingImageAsset> diffuseImageAsset =
+            AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::StreamingImageAsset>
+            (DiffuseAssetPath, assertTraceLevel);
+        AZ::Data::Asset<AZ::RPI::StreamingImageAsset> specularImageAsset =
+            AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::StreamingImageAsset>
+            (SpecularAssetPath, assertTraceLevel);
+
+        auto featureProcessor = scene->GetFeatureProcessor<AZ::Render::ImageBasedLightFeatureProcessorInterface>();
+        AZ_Assert(featureProcessor, "Unable to find ImageBasedLightFeatureProcessorInterface on scene.");
+
+        featureProcessor->SetDiffuseImage(diffuseImageAsset);
+        featureProcessor->SetSpecularImage(specularImageAsset);
+    }
+
+    void DiffuseGIExampleComponent::EnableDiffuseIbl()
+    {
+        AZ::RPI::ScenePtr scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        AZ::Render::ImageBasedLightFeatureProcessorInterface* imageBaseLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::ImageBasedLightFeatureProcessorInterface>();
+        AZ_Assert(imageBaseLightFeatureProcessor, "Unable to find ImageBasedLightFeatureProcessorInterface on scene.");
+
+        imageBaseLightFeatureProcessor->SetDiffuseImage(m_diffuseImageAsset);
+        imageBaseLightFeatureProcessor->SetExposure(m_diffuseIblExposure);
+    }
+} // namespace AtomSampleViewer

+ 187 - 0
Gem/Code/Source/DiffuseGIExampleComponent.h

@@ -0,0 +1,187 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzFramework/Components/CameraBus.h>
+#include <CommonSampleComponentBase.h>
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+#include <Utils/Utils.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/ImGuiMaterialDetails.h>
+#include <Utils/ImGuiAssetBrowser.h>
+#include <Atom/RPI.Public/WindowContext.h>
+#include <Atom/Feature/Mesh/MeshFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/PointLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Feature/DiffuseProbeGrid/DiffuseProbeGridFeatureProcessorInterface.h>
+
+namespace AtomSampleViewer
+{
+    //! This sample demonstrates diffuse global illumination using the DiffuseProbeGrid.
+    class DiffuseGIExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(AtomSampleViewer::DiffuseGIExampleComponent, "{46E0E36D-707D-42D6-B4CB-08A19F576299}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        DiffuseGIExampleComponent() = default;
+        ~DiffuseGIExampleComponent() override = default;
+
+        // Component
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        AZ_DISABLE_COPY_MOVE(DiffuseGIExampleComponent);
+
+        // TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+        // EntityBus::MultiHandler
+        void OnEntityDestruction(const AZ::EntityId& entityId) override;
+
+        float ComputePointLightAttenuationRadius(AZ::Color color, float intensity);
+        void LoadSampleSceneAssets();
+        void CreateCornellBoxScene(bool geometryOnly);
+        void CreateBistroScene();
+        void UpdateImGui();
+        void SetSampleScene(bool geometryOnly = false);
+        void UnloadSampleScene(bool geometryOnly);
+        void DisableGlobalIbl();
+        void EnableDiffuseIbl();
+
+        // camera
+        bool m_resetCamera = true;
+        float m_originalFarClipDistance = 0.0f;
+        AZ::Vector3 m_cameraTranslation = AZ::Vector3(0.0f);
+
+        Utils::DefaultIBL m_defaultIbl;
+
+        // directional light
+        float m_directionalLightPitch = -AZ::Constants::QuarterPi;
+        float m_directionalLightYaw = 0.f;
+        float m_directionalLightIntensity = 20.0f;
+        AZ::Color m_directionalLightColor;
+
+        // point light
+        AZ::Vector3 m_pointLightPos;
+        AZ::Color m_pointLightColor;
+        float m_pointLightIntensity = 20.0f;
+
+        // ImGui
+        ImGuiSidebar m_imguiSidebar;
+
+        enum SampleScene
+        {
+            None,
+            CornellBox,
+            Bistro
+        };
+
+        SampleScene m_sampleScene = SampleScene::None;
+
+        // CornellBox scene
+        enum class CornellBoxMeshes
+        {
+            LeftWall,
+            RightWall,
+            BackWall,
+            Ceiling,
+            Floor,
+            LargeBox,
+            SmallBox,
+            Count
+        };
+
+        enum class CornellBoxColors
+        {
+            Red,
+            Green,
+            Blue,
+            Yellow,
+            White,
+            Count
+        };
+
+        static const char* CornellBoxColorNames[aznumeric_cast<uint32_t>(CornellBoxColors::Count)];
+        static const uint32_t CornellBoxColorNamesCount = sizeof(CornellBoxColorNames) / sizeof(CornellBoxColorNames[0]);
+
+        AZ::Data::Asset<AZ::RPI::MaterialAsset>& GetCornellBoxMaterialAsset(CornellBoxColors color);
+
+        // models
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_planeModelAsset;
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_cubeModelAsset;
+
+        // materials
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_redMaterialAsset;
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_greenMaterialAsset;
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_blueMaterialAsset;
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_yellowMaterialAsset;
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_whiteMaterialAsset;
+
+        // wall visibility
+        bool m_leftWallVisible = true;
+        bool m_rightWallVisible = true;
+        bool m_backWallVisible = true;
+        bool m_floorVisible = true;
+        bool m_ceilingVisible = true;
+
+        // wall material colors
+        CornellBoxColors m_leftWallColor = CornellBoxColors::Red;
+        CornellBoxColors m_rightWallColor = CornellBoxColors::Green;
+        CornellBoxColors m_backWallColor = CornellBoxColors::White;
+        CornellBoxColors m_floorColor = CornellBoxColors::White;
+        CornellBoxColors m_ceilingColor = CornellBoxColors::White;
+
+        // Bistro scene
+        enum class BistroMeshes
+        {
+            Inside,
+            Outside,
+            Count
+        };
+
+        // models
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_bistroInteriorModelAsset;
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_bistroExteriorModelAsset;
+
+        // scene
+        AZStd::vector<AZ::Render::MeshFeatureProcessorInterface::MeshHandle> m_meshHandles;
+        AZ::Render::PointLightFeatureProcessorInterface::LightHandle m_pointLightHandle;
+        AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle m_directionalLightHandle;
+        AZ::Render::DiffuseProbeGridHandle m_diffuseProbeGrid;
+
+        // diffuse IBL (Bistro only)
+        bool m_useDiffuseIbl = true;
+        AZ::Data::Asset<AZ::RPI::StreamingImageAsset> m_diffuseImageAsset;
+        float m_diffuseIblExposure = 1.0f;
+
+        // shadow settings
+        static const AZ::Render::ShadowmapSize s_shadowmapSizes[];
+        static const char* s_directionalLightShadowmapSizeLabels[];
+        static constexpr int s_shadowmapSizeIndexDefault = 3;
+        static constexpr int s_cascadesCountDefault = 4;
+
+        // GI settings
+        bool m_enableDiffuseGI = true;
+        float m_viewBias = 0.4f;
+        float m_normalBias = 0.1f;
+        float m_ambientMultiplier = 1.0f;
+        AZ::Vector3 m_origin;
+        bool m_giShadows = true;
+    };
+} // namespace AtomSampleViewer

+ 288 - 0
Gem/Code/Source/DynamicDrawExampleComponent.cpp

@@ -0,0 +1,288 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <DynamicDrawExampleComponent.h>
+
+#include <SampleComponentManager.h>
+
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/RPI.Public/DynamicDraw/DynamicDrawInterface.h>
+#include <Atom/RPI.Public/RPIUtils.h>
+
+namespace AtomSampleViewer
+{
+    void DynamicDrawExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext * serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<DynamicDrawExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    DynamicDrawExampleComponent::DynamicDrawExampleComponent()
+    {
+        m_sampleName = "DynamicDrawExampleComponent";
+    }
+
+    void DynamicDrawExampleComponent::Activate()
+    {
+        using namespace AZ;
+
+        // List of all assets this example needs.
+        AZStd::vector<AZ::AssetCollectionAsyncLoader::AssetToLoadInfo> assetList = {
+            {"Shaders/dynamicdraw/dynamicdrawexample.azshader", azrtti_typeid<AZ::RPI::ShaderAsset>()},
+        };
+
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScript);
+
+        PreloadAssets(assetList);
+    }
+
+    void DynamicDrawExampleComponent::OnAllAssetsReadyActivate()
+    {
+        AZ::TickBus::Handler::BusConnect();
+
+        m_imguiSidebar.Activate();
+
+        using namespace AZ;
+
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+
+        AZ::Debug::NoClipControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetPosition, Vector3(-0.11f, -3.01f, -0.02f));
+        AZ::Debug::NoClipControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetHeading, DegToRad(-4.0f));
+        AZ::Debug::NoClipControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetPitch, DegToRad(1.9f));
+
+        // Create and initialize dynamic draw context
+        m_dynamicDraw = RPI::DynamicDrawInterface::Get()->CreateDynamicDrawContext(RPI::RPISystemInterface::Get()->GetDefaultScene().get());
+        
+        const char* shaderFilepath = "Shaders/dynamicdraw/dynamicdrawexample.azshader";
+        Data::Asset<RPI::ShaderAsset> shaderAsset = m_assetLoadManager.GetAsset<RPI::ShaderAsset>(shaderFilepath);
+        m_dynamicDraw->InitShader(shaderAsset);
+        m_dynamicDraw->InitVertexFormat(
+            {{ "POSITION", RHI::Format::R32G32B32_FLOAT },
+            { "COLOR", RHI::Format::R32G32B32A32_FLOAT }}
+            );
+        m_dynamicDraw->AddDrawStateOptions(RPI::DynamicDrawContext::DrawStateOptions::BlendMode | RPI::DynamicDrawContext::DrawStateOptions::PrimitiveType
+            | RPI::DynamicDrawContext::DrawStateOptions::DepthState | RPI::DynamicDrawContext::DrawStateOptions::FaceCullMode);
+        m_dynamicDraw->EndInit();
+
+        Data::Instance<RPI::ShaderResourceGroup> contextSrg = m_dynamicDraw->GetPerContextSrg();
+        if (contextSrg)
+        {
+            auto index = contextSrg->FindShaderInputConstantIndex(Name("m_scale"));
+            contextSrg->SetConstant(index, 1);
+            contextSrg->Compile();
+        }
+
+        AZ_Assert(m_dynamicDraw->IsVertexSizeValid(sizeof(ExampleVertex)), "Invalid vertex format");
+
+
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+    }
+
+    void DynamicDrawExampleComponent::Deactivate()
+    {
+        using namespace AZ;
+
+        m_imguiSidebar.Deactivate();
+
+        TickBus::Handler::BusDisconnect();
+
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+
+        m_dynamicDraw = nullptr;
+        m_contextSrg = nullptr;
+    }
+
+    void DynamicDrawExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        using namespace AZ;
+
+        if (m_imguiSidebar.Begin())
+        {
+            ScriptableImGui::Checkbox("CullMode::None", &m_showCullModeNull);
+            ScriptableImGui::Checkbox("CullMode: Front", &m_showCullModeFront);
+            ScriptableImGui::Checkbox("CullMode: Back", &m_showCullModeBack);
+            ScriptableImGui::Checkbox("PrimitiveTopology: LineList", &m_showLineList);
+            ScriptableImGui::Checkbox("Alpha Blend", &m_showAlphaBlend);
+            ScriptableImGui::Checkbox("Alpha Additive", &m_showAlphaAdditive);
+            ScriptableImGui::Checkbox("Per Draw Viewport", &m_showPerDrawViewport);
+
+            m_imguiSidebar.End();
+        }
+
+        // draw srg with default offset
+        Data::Instance<RPI::ShaderResourceGroup> drawSrg = m_dynamicDraw->NewDrawSrg();
+        auto index = drawSrg->FindShaderInputConstantIndex(Name("m_positionOffset"));
+        drawSrg->SetConstant(index, Vector3(0, 0, 0));
+        drawSrg->Compile();
+
+        // Tetrahedron
+        const uint32_t TetrahedronVertexCount = 12;
+        const uint32_t TetrahedronIndexCount = 12;
+        const uint32_t TetrahedronWireframeIndexCount = 6;
+
+        ExampleVertex tetrahedronVerts[TetrahedronVertexCount] = {
+            {0.2, 0.2f, 0.2f,       1, 0, 0, 0.5f}, // 0
+            {-0.2, -0.2f, 0.2f,     1, 0, 0, 0.5f}, // 3
+            {0.2, -0.2f, -0.2f,     1, 0, 0, 0.5f}, // 1
+
+            {0.2, 0.2f, 0.2f,       0, 1, 0, 0.5f}, // 0
+            {0.2, -0.2f, -0.2f,     0, 1, 0, 0.5f}, // 1
+            {-0.2, 0.2f, -0.2f,     0, 1, 0, 0.5f}, // 2
+
+            {0.2, 0.2f, 0.2f,       0, 0, 1, 0.5f}, // 0
+            {-0.2, 0.2f, -0.2f,     0, 0, 1, 0.5f}, // 2
+            {-0.2, -0.2f, 0.2f,     0, 0, 1, 0.5f}, // 3
+
+            {0.2, -0.2f, -0.2f,     1, 1, 0, 0.5f}, // 1
+            {-0.2, -0.2f, 0.2f,     1, 1, 0, 0.5f}, // 3
+            {-0.2, 0.2f, -0.2f,     1, 1, 0, 0.5f}  // 2
+        };
+        u16 tetrahedronIndices[TetrahedronIndexCount] = {
+            0, 1, 2,
+            3, 4, 5,
+            6, 7, 8,
+            9, 10, 11
+        };
+
+        u16 tetrahedronWireframeIndices[TetrahedronIndexCount] =
+        {
+            0, 1,
+            0, 2,
+            0, 4,
+            1, 2,
+            2, 4,
+            4, 1
+        };
+
+        // Enable depth test and write
+        RHI::DepthState depthState;
+        depthState.m_enable = true;
+        depthState.m_writeMask = RHI::DepthWriteMask::All;
+        depthState.m_func = RHI::ComparisonFunc::GreaterEqual;
+        m_dynamicDraw->SetDepthState(depthState);
+        // Disable blend
+        RHI::TargetBlendState blendState;
+        blendState.m_enable = false;
+        m_dynamicDraw->SetTarget0BlendState(blendState);
+
+        float xPos = -1.5f;
+        const float xOffset = 0.5f;
+
+        // no cull
+        if (m_showCullModeNull)
+        {
+            m_dynamicDraw->SetCullMode(RHI::CullMode::None);
+            drawSrg = m_dynamicDraw->NewDrawSrg();
+            drawSrg->SetConstant(index, Vector3(xPos, 0, 0));
+            drawSrg->Compile();
+            m_dynamicDraw->DrawIndexed(tetrahedronVerts, TetrahedronVertexCount, tetrahedronIndices, TetrahedronIndexCount, RHI::IndexFormat::Uint16, drawSrg);
+        }
+
+        //front cull
+        xPos += xOffset;
+        if (m_showCullModeFront)
+        {
+            m_dynamicDraw->SetCullMode(RHI::CullMode::Front);
+            drawSrg = m_dynamicDraw->NewDrawSrg();
+            drawSrg->SetConstant(index, Vector3(xPos, 0, 0));
+            drawSrg->Compile();
+            m_dynamicDraw->DrawIndexed(tetrahedronVerts, TetrahedronVertexCount, tetrahedronIndices, TetrahedronIndexCount, RHI::IndexFormat::Uint16, drawSrg);
+            m_dynamicDraw->SetCullMode(RHI::CullMode::None);
+        }
+
+        // back cull
+        xPos += xOffset;
+        if (m_showCullModeBack)
+        {
+            m_dynamicDraw->SetCullMode(RHI::CullMode::Back);
+            drawSrg = m_dynamicDraw->NewDrawSrg();
+            drawSrg->SetConstant(index, Vector3(xPos, 0, 0));
+            drawSrg->Compile();
+            m_dynamicDraw->DrawIndexed(tetrahedronVerts, TetrahedronVertexCount, tetrahedronIndices, TetrahedronIndexCount, RHI::IndexFormat::Uint16, drawSrg);
+            m_dynamicDraw->SetCullMode(RHI::CullMode::None);
+        }
+
+        // Draw line lists
+        xPos += xOffset;
+        if (m_showLineList)
+        {
+            drawSrg = m_dynamicDraw->NewDrawSrg();
+            drawSrg->SetConstant(index, Vector3(xPos, 0, 0));
+            drawSrg->Compile();
+            m_dynamicDraw->SetPrimitiveType(RHI::PrimitiveTopology::LineList);
+            m_dynamicDraw->DrawIndexed(tetrahedronVerts, TetrahedronVertexCount, tetrahedronWireframeIndices, TetrahedronIndexCount, RHI::IndexFormat::Uint16, drawSrg);
+            m_dynamicDraw->SetPrimitiveType(RHI::PrimitiveTopology::TriangleList);
+        }
+
+        // disable depth write
+        depthState.m_writeMask = RHI::DepthWriteMask::Zero;
+        m_dynamicDraw->SetDepthState(depthState);
+
+        // alpha blend
+        xPos += xOffset;
+        if (m_showAlphaBlend)
+        {
+            blendState.m_enable = true;
+            blendState.m_blendOp = RHI::BlendOp::Add;
+            blendState.m_blendSource = RHI::BlendFactor::AlphaSource;
+            blendState.m_blendDest = RHI::BlendFactor::AlphaSourceInverse;
+            m_dynamicDraw->SetTarget0BlendState(blendState);
+            drawSrg = m_dynamicDraw->NewDrawSrg();
+            drawSrg->SetConstant(index, Vector3(xPos, 0, 0));
+            drawSrg->Compile();
+            m_dynamicDraw->DrawIndexed(tetrahedronVerts, TetrahedronVertexCount, tetrahedronIndices, TetrahedronIndexCount, RHI::IndexFormat::Uint16, drawSrg);
+        }
+
+        // alpha additive
+        xPos += xOffset;
+        if (m_showAlphaAdditive)
+        {
+            blendState.m_enable = true;
+            blendState.m_blendOp = RHI::BlendOp::Add;
+            blendState.m_blendSource = RHI::BlendFactor::AlphaSource;
+            blendState.m_blendDest = RHI::BlendFactor::One;
+            m_dynamicDraw->SetTarget0BlendState(blendState);
+            drawSrg = m_dynamicDraw->NewDrawSrg();
+            drawSrg->SetConstant(index, Vector3(xPos, 0, 0));
+            drawSrg->Compile();
+            m_dynamicDraw->DrawIndexed(tetrahedronVerts, TetrahedronVertexCount, tetrahedronIndices, TetrahedronIndexCount, RHI::IndexFormat::Uint16, drawSrg);
+        }
+
+        // enable depth write
+        depthState.m_writeMask = RHI::DepthWriteMask::All;
+        m_dynamicDraw->SetDepthState(depthState);
+        // disable blend
+        blendState.m_enable = false;
+        m_dynamicDraw->SetTarget0BlendState(blendState);
+
+        // per draw viewport
+        xPos += xOffset;
+        if (m_showPerDrawViewport)
+        {
+            m_dynamicDraw->SetViewport(RHI::Viewport(0, 200, 0, 200));
+            drawSrg = m_dynamicDraw->NewDrawSrg();
+            drawSrg->SetConstant(index, Vector3(0, 0, 0));
+            drawSrg->Compile();
+            m_dynamicDraw->DrawIndexed(tetrahedronVerts, TetrahedronVertexCount, tetrahedronIndices, TetrahedronIndexCount, RHI::IndexFormat::Uint16, drawSrg);
+            m_dynamicDraw->UnsetViewport();
+        }
+    }
+} // namespace AtomSampleViewer

+ 74 - 0
Gem/Code/Source/DynamicDrawExampleComponent.h

@@ -0,0 +1,74 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <AzCore/Component/TickBus.h>
+
+#include <Atom/RPI.Public/Buffer/Buffer.h>
+#include <Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h>
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+
+#include <Atom/RHI/StreamBufferView.h>
+#include <Atom/RHI/IndexBufferView.h>
+#include <Atom/RHI/PipelineState.h>
+#include <Atom/RHI/DrawList.h>
+
+#include <Utils/ImGuiSidebar.h>
+
+namespace AtomSampleViewer
+{
+    //! Provides a basic example for how to use DynamicDrawInterface and DynamicDrawContext
+    class DynamicDrawExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(DynamicDrawExampleComponent, "{0BA35CA5-31A4-422B-A269-E138EDD0BB5F}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+        DynamicDrawExampleComponent();
+        ~DynamicDrawExampleComponent() override = default;
+
+        // AZ::Component overrides ...
+        void Activate() override;
+        void Deactivate() override;
+
+        // AZ::TickBus::Handler overrides ...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+    private:
+        struct ExampleVertex
+        {
+            float x, y, z;
+            float r, g, b, a;
+        };
+
+        AZ::RHI::Ptr<AZ::RPI::DynamicDrawContext> m_dynamicDraw;
+        AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> m_contextSrg;
+
+        ImGuiSidebar m_imguiSidebar;
+
+        bool m_showCullModeNull = true;
+        bool m_showCullModeFront = true;
+        bool m_showCullModeBack = true;
+        bool m_showAlphaBlend = true;
+        bool m_showAlphaAdditive = true;
+        bool m_showLineList = true;
+        bool m_showPerDrawViewport = true;
+
+        // CommonSampleComponentBase overrides...
+        void OnAllAssetsReadyActivate() override;
+    };
+} // namespace AtomSampleViewer

+ 308 - 0
Gem/Code/Source/DynamicMaterialTestComponent.cpp

@@ -0,0 +1,308 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <DynamicMaterialTestComponent.h>
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <AzCore/Component/Entity.h>
+#include <AzCore/Debug/EventTrace.h>
+#include <AzCore/Math/Random.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+    using namespace RPI;
+
+    void DynamicMaterialTestComponent::Reflect(ReflectContext* context)
+    {
+        if (SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context))
+        {
+            serializeContext->Class<DynamicMaterialTestComponent, EntityLatticeTestComponent>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    DynamicMaterialTestComponent::DynamicMaterialTestComponent()
+        : m_imguiSidebar("@user@/DynamicMaterialTestComponent/sidebar.xml")
+        , m_compileTimer(CompileTimerQueueSize, CompileTimerQueueSize)
+    {
+
+    }
+
+    void DynamicMaterialTestComponent::Activate()
+    {
+        TickBus::Handler::BusConnect();
+        m_imguiSidebar.Activate();
+        InitMaterialConfigs();
+        Base::Activate();
+
+        m_currentTime = 0.0f;
+    }
+
+    void DynamicMaterialTestComponent::Deactivate()
+    {
+        TickBus::Handler::BusDisconnect();
+        m_imguiSidebar.Deactivate();
+        Base::Deactivate();
+    }
+    
+    void DynamicMaterialTestComponent::PrepareCreateLatticeInstances(uint32_t instanceCount)
+    {
+        const char* modelPath = "objects/shaderball_simple.azmodel";
+
+        Data::AssetId modelAssetId;
+        Data::AssetCatalogRequestBus::BroadcastResult(
+            modelAssetId, &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
+            modelPath, azrtti_typeid<ModelAsset>(), false);
+
+        AZ_Assert(modelAssetId.IsValid(), "Failed to get model asset id: %s", modelPath);
+
+        m_modelAsset.Create(modelAssetId);
+
+        m_meshHandles.reserve(instanceCount);
+        m_materials.reserve(instanceCount);
+
+        // Give the models time to load before continuing scripts
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScript);
+        m_waitingForMeshes = true;
+    }
+
+    void DynamicMaterialTestComponent::CreateLatticeInstance(const Transform& transform)
+    {
+        AZ::Data::Asset<AZ::RPI::MaterialAsset>& materialAsset = m_materialConfigs[m_currentMaterialConfig].m_materialAsset;
+        AZ::Data::Instance<AZ::RPI::Material> material = Material::Create(materialAsset);
+         
+        Render::MaterialAssignmentMap materialMap;
+        Render::MaterialAssignment& defaultMaterial = materialMap[Render::DefaultMaterialAssignmentId];
+        defaultMaterial.m_materialAsset = materialAsset;
+        defaultMaterial.m_materialInstance = material;
+
+        auto meshHandle = GetMeshFeatureProcessor()->AcquireMesh(m_modelAsset, materialMap, false, false);
+        GetMeshFeatureProcessor()->SetTransform(meshHandle, transform);
+
+        Data::Instance<RPI::Model> model = GetMeshFeatureProcessor()->GetModel(meshHandle);
+        if (model)
+        {
+            m_loadedMeshCounter++;
+        }
+        else
+        {
+            m_meshLoadEventHandlers.push_back(AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler
+                {
+                    [this](AZ::Data::Instance<AZ::RPI::Model> model) { m_loadedMeshCounter++; }
+                });
+            GetMeshFeatureProcessor()->ConnectModelChangeEventHandler(meshHandle, m_meshLoadEventHandlers.back());
+        }
+        
+        m_meshHandles.push_back(AZStd::move(meshHandle));
+        m_materials.push_back(material);
+    }
+
+    void DynamicMaterialTestComponent::DestroyLatticeInstances()
+    {
+        for (auto& meshHandle : m_meshHandles)
+        {
+            GetMeshFeatureProcessor()->ReleaseMesh(meshHandle);
+        }
+        m_meshHandles.clear();
+        m_materials.clear();
+
+        m_loadedMeshCounter = 0;
+        m_waitingForMeshes = false;
+        m_meshLoadEventHandlers.clear();
+    }
+
+    void DynamicMaterialTestComponent::InitMaterialConfigs()
+    {
+        using namespace AZ::RPI;
+
+        MaterialConfig config;
+
+        config.m_name = "Default StandardPBR Material";
+        config.m_materialAsset = AssetUtils::GetAssetByProductPath<MaterialAsset>(DefaultPbrMaterialPath, AssetUtils::TraceLevel::Assert);
+        config.m_updateLatticeMaterials = [this]() { UpdateStandardPbrColors(); };
+        m_materialConfigs.push_back(config);
+
+        config.m_name = "C++ Functor Test Material";
+        config.m_materialAsset = AssetUtils::GetAssetByProductPath<MaterialAsset>("materials/dynamicmaterialtest/emissivewithcppfunctors.azmaterial", AssetUtils::TraceLevel::Assert);
+        config.m_updateLatticeMaterials = [this]() { UpdateEmissiveMaterialIntensity(); };
+        m_materialConfigs.push_back(config);
+
+        config.m_name = "Lua Functor Test Material";
+        config.m_materialAsset = AssetUtils::GetAssetByProductPath<MaterialAsset>("materials/dynamicmaterialtest/emissivewithluafunctors.azmaterial", AssetUtils::TraceLevel::Assert);
+        config.m_updateLatticeMaterials = [this]() { UpdateEmissiveMaterialIntensity(); };
+        m_materialConfigs.push_back(config);
+
+        m_currentMaterialConfig = 0;
+    }
+
+    void DynamicMaterialTestComponent::UpdateStandardPbrColors()
+    {
+        static const Color colorOptions[] =
+        {
+            Color(1.0f, 0.0f, 0.0f, 1.0f),
+            Color(0.0f, 1.0f, 0.0f, 1.0f),
+            Color(0.0f, 0.0f, 1.0f, 1.0f),
+            Color(1.0f, 1.0f, 0.0f, 1.0f),
+            Color(0.0f, 1.0f, 1.0f, 1.0f),
+            Color(1.0f, 0.0f, 1.0f, 1.0f),
+        };
+
+        // Create a new SimpleLcgRandom every time to keep a consistent seed and consistent color selection.
+        SimpleLcgRandom random;
+
+        for (int i = 0; i < m_meshHandles.size(); ++i)
+        {
+            auto& meshHandle = m_meshHandles[i];
+            auto& material = m_materials[i];
+
+            static const float CylesPerSecond = 0.5f;
+            const float t = aznumeric_cast<float>(sin(m_currentTime * CylesPerSecond * AZ::Constants::TwoPi) * 0.5f + 0.5f);
+
+            const int colorIndexA = random.GetRandom() % AZ_ARRAY_SIZE(colorOptions);
+            int colorIndexB = colorIndexA;
+            while (colorIndexA == colorIndexB)
+            {
+                colorIndexB = random.GetRandom() % AZ_ARRAY_SIZE(colorOptions);
+            }
+            const Color color = colorOptions[colorIndexA] * t + colorOptions[colorIndexB] * (1.0f - t);
+
+            MaterialPropertyIndex colorProperty = material->FindPropertyIndex(AZ::Name{"baseColor.color"});
+            material->SetPropertyValue(colorProperty, color);
+        }
+    }
+
+    void DynamicMaterialTestComponent::UpdateEmissiveMaterialIntensity()
+    {
+        for (int i = 0; i < m_meshHandles.size(); ++i)
+        {
+            auto& meshHandle = m_meshHandles[i];
+            auto& material = m_materials[i];
+
+            const float distance = GetMeshFeatureProcessor()->GetTransform(meshHandle).GetTranslation().GetLengthEstimate();
+
+            static const float DistanceScale = 0.02f;
+            static const float CylesPerSecond = 0.5f;
+
+            const float t = aznumeric_cast<float>(sin((DistanceScale * distance + m_currentTime * CylesPerSecond) * AZ::Constants::TwoPi) * 0.5f + 0.5f);
+
+            static const float MinIntensity = 1.0f;
+            static const float MaxIntensity = 4.0f;
+            const float intensity = AZ::Lerp(MinIntensity, MaxIntensity, t);
+
+            MaterialPropertyIndex intensityProperty = material->FindPropertyIndex(AZ::Name{"emissive.intensity"});
+            material->SetPropertyValue(intensityProperty, intensity);
+        }
+    }
+
+    void DynamicMaterialTestComponent::CompileMaterials()
+    {
+        AZ::Debug::Timer timer;
+        timer.Stamp();
+
+        for (auto& material : m_materials)
+        {
+            material->Compile();
+        }
+
+        m_compileTimer.PushValue(timer.GetDeltaTimeInSeconds() * 1'000'000);
+    }
+
+    void DynamicMaterialTestComponent::OnTick(float deltaTime, ScriptTimePoint /*scriptTime*/)
+    {
+        AZ_TRACE_METHOD();
+
+        if (m_waitingForMeshes)
+        {
+            if(m_loadedMeshCounter == m_meshHandles.size())
+            {
+                m_waitingForMeshes = false;
+                ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+
+                // Reset the clock so we get consistent animation for scripts
+                m_currentTime = 0;
+            }
+        }
+
+        AZ_Assert(m_loadedMeshCounter <= m_meshHandles.size(), "Mesh load handlers were called multiple times?");
+
+        bool updateMaterials = false;
+
+        if (!m_pause)
+        {
+            m_currentTime += deltaTime;
+            updateMaterials = true;
+        }
+
+        if (m_imguiSidebar.Begin())
+        {
+            RenderImGuiLatticeControls();
+
+            ImGui::Separator();
+
+            bool configChanged = false;
+
+            for (int i = 0; i < m_materialConfigs.size(); ++i)
+            {
+                configChanged = configChanged || ScriptableImGui::RadioButton(m_materialConfigs[i].m_name.c_str(), &m_currentMaterialConfig, i);
+            }
+
+            if (configChanged)
+            {
+                RebuildLattice();
+            }
+
+            ImGui::Separator();
+
+            ScriptableImGui::Checkbox("Pause", &m_pause);
+            
+            if (ScriptableImGui::Button("Reset Clock"))
+            {
+                m_currentTime = 0;
+                updateMaterials = true;
+            }
+
+            ImGui::Separator();
+
+            ImGui::Text("%d unique objects", aznumeric_cast<int32_t>(m_meshHandles.size()));
+
+            ImGui::Text("Total Material Compile Time:");
+
+            ImGuiHistogramQueue::WidgetSettings settings;
+            settings.m_units = "microseconds";
+            m_compileTimer.Tick(deltaTime, settings);
+
+            ImGui::Text("Average per Material: %4.2f", m_compileTimer.GetDisplayedAverage() / m_materials.size());
+
+            ImGui::Separator();
+
+            m_imguiSidebar.End();
+        }
+
+        if (updateMaterials)
+        {
+            m_materialConfigs[m_currentMaterialConfig].m_updateLatticeMaterials();
+            CompileMaterials();
+        }
+    }
+} // namespace AtomSampleViewer

+ 87 - 0
Gem/Code/Source/DynamicMaterialTestComponent.h

@@ -0,0 +1,87 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <EntityLatticeTestComponent.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/ImGuiHistogramQueue.h>
+#include <Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h>
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/std/functional.h>
+
+namespace AtomSampleViewer
+{
+    //! This test loads a configurable lattice of entities, gives them all a unique Material instance, and
+    //! changes a material property value every frame. UI to configure the size of the lattice is included.
+    class DynamicMaterialTestComponent final
+        : public EntityLatticeTestComponent
+        , public AZ::TickBus::Handler
+    {
+        using Base = EntityLatticeTestComponent;
+
+    public:
+        AZ_COMPONENT(DynamicMaterialTestComponent, "{14E4AD00-BB56-4771-809F-1AF7BD3611D9}", EntityLatticeTestComponent);
+        DynamicMaterialTestComponent();
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        // AZ::Component overrides...
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        AZ_DISABLE_COPY_MOVE(DynamicMaterialTestComponent);
+
+        void InitMaterialConfigs();
+
+        //! EntityLatticeTestComponent overrides...
+        void PrepareCreateLatticeInstances(uint32_t instanceCount) override;
+        void CreateLatticeInstance(const AZ::Transform& transform) override;
+        void DestroyLatticeInstances() override;
+        
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint scriptTime) override;
+
+        void CompileWithTimer(AZ::Data::Instance<AZ::RPI::Material> material);
+
+        void UpdateStandardPbrColors();
+        void UpdateEmissiveMaterialIntensity();
+        void CompileMaterials();
+
+        ImGuiSidebar m_imguiSidebar;
+        bool m_pause = false;
+
+        struct MaterialConfig
+        {
+            AZStd::string m_name;
+            AZ::Data::Asset<AZ::RPI::MaterialAsset> m_materialAsset;
+            AZStd::function<void()> m_updateLatticeMaterials;
+        };
+        AZStd::vector<MaterialConfig> m_materialConfigs;
+        int m_currentMaterialConfig;
+
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_modelAsset;
+        AZStd::vector<AZ::Render::MeshFeatureProcessorInterface::MeshHandle> m_meshHandles;
+        AZStd::vector<AZ::Data::Instance<AZ::RPI::Material>> m_materials;
+
+        static constexpr AZStd::size_t CompileTimerQueueSize = 30;
+        ImGuiHistogramQueue m_compileTimer;
+
+        bool m_waitingForMeshes = false;
+        uint32_t m_loadedMeshCounter = 0;
+        AZStd::vector<AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler> m_meshLoadEventHandlers;
+
+        float m_currentTime = 0.0f;
+    };
+} // namespace AtomSampleViewer

+ 129 - 0
Gem/Code/Source/EntityLatticeTestComponent.cpp

@@ -0,0 +1,129 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <EntityLatticeTestComponent.h>
+
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <AzCore/Component/Entity.h>
+
+#include <AzFramework/Components/TransformComponent.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <Automation/ScriptableImGui.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+    using namespace RPI;
+
+    constexpr int32_t s_latticeSizeMax = 20;
+
+    void EntityLatticeTestComponent::Reflect(ReflectContext* context)
+    {
+        if (SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context))
+        {
+            serializeContext->Class<EntityLatticeTestComponent, Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    void EntityLatticeTestComponent::Activate()
+    {
+        Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<Debug::NoClipControllerComponent>());
+
+        m_defaultIbl.Init(RPI::RPISystemInterface::Get()->GetDefaultScene().get());
+        BuildLattice();
+    }
+
+    void EntityLatticeTestComponent::Deactivate()
+    {
+        Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Disable);
+
+        DestroyLatticeInstances();
+        m_defaultIbl.Reset();
+    }
+
+    void EntityLatticeTestComponent::RebuildLattice()
+    {
+        DestroyLatticeInstances();
+        BuildLattice();
+    }
+
+    void EntityLatticeTestComponent::BuildLattice()
+    {
+        PrepareCreateLatticeInstances(GetInstanceCount());
+
+        // We first rotate the model by 180 degrees before translating it. This is to make it face the camera as it did
+        // when the world was Y-up.
+        Transform transform = Transform::CreateRotationZ(Constants::Pi);
+
+        static Vector3 distance(5.0f, 5.0f, 5.0f);
+        for (int32_t x = 0; x < m_latticeWidth; ++x)
+        {
+            for (int32_t y = 0; y < m_latticeDepth; ++y)
+            {
+                for (int32_t z = 0; z < m_latticeHeight; ++z)
+                {
+                    Vector3 position(
+                        static_cast<float>(x) * distance.GetX(),
+                        static_cast<float>(y) * distance.GetY(),
+                        static_cast<float>(z) * distance.GetZ());
+
+                    transform.SetTranslation(position);
+                    CreateLatticeInstance(transform);
+                }
+            }
+        }
+        FinalizeLatticeInstances();
+    }
+
+    uint32_t EntityLatticeTestComponent::GetInstanceCount() const
+    {
+        return m_latticeWidth * m_latticeHeight * m_latticeDepth;
+    }
+
+    void EntityLatticeTestComponent::SetLatticeDimensions(uint32_t width, uint32_t depth, uint32_t height)
+    {
+        m_latticeWidth = GetClamp<int32_t>(width, 1, s_latticeSizeMax);
+        m_latticeHeight = GetClamp<int32_t>(height, 1, s_latticeSizeMax);
+        m_latticeDepth = GetClamp<int32_t>(depth, 1, s_latticeSizeMax);
+    }
+
+    void EntityLatticeTestComponent::RenderImGuiLatticeControls()
+    {
+        bool latticeChanged = false;
+
+        ImGui::Text("Lattice Width");
+        latticeChanged |= ScriptableImGui::SliderInt("##LatticeWidth", &m_latticeWidth, 1, s_latticeSizeMax);
+
+        ImGui::Spacing();
+
+        ImGui::Text("Lattice Height");
+        latticeChanged |= ScriptableImGui::SliderInt("##LatticeHeight", &m_latticeHeight, 1, s_latticeSizeMax);
+
+        ImGui::Spacing();
+
+        ImGui::Text("Lattice Depth");
+        latticeChanged |= ScriptableImGui::SliderInt("##LatticeDepth", &m_latticeDepth, 1, s_latticeSizeMax);
+
+        if (latticeChanged)
+        {
+            RebuildLattice();
+        }
+    }
+} // namespace AtomSampleViewer

+ 72 - 0
Gem/Code/Source/EntityLatticeTestComponent.h

@@ -0,0 +1,72 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+#include <Utils/Utils.h>
+
+struct ImGuiContext;
+
+namespace AtomSampleViewer
+{
+    //! Common base class for test components that display a lattice of entities.
+    class EntityLatticeTestComponent
+        : public CommonSampleComponentBase
+    {
+    public:
+        AZ_RTTI(EntityLatticeTestComponent, "{73C13F66-6F5B-43D3-B1F0-CB4F7BEA1334}", CommonSampleComponentBase)
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        // AZ::Component overrides...
+        void Activate() override;
+        void Deactivate() override;
+
+    protected:
+
+        //! Returns total number of instances (width * height * depth)
+        uint32_t GetInstanceCount() const;
+        
+        //! Call this to render ImGui controls for controlling the size of the lattice.
+        void RenderImGuiLatticeControls();
+        
+        //! Destroys and rebuilds the lattice.
+        virtual void RebuildLattice();
+
+        void SetLatticeDimensions(uint32_t width, uint32_t depth, uint32_t height);
+
+    private:
+
+        //! Called once before CreateLatticeInstance() is called for each instance so the subclass can prepare for the total number of instances.
+        virtual void PrepareCreateLatticeInstances(uint32_t instanceCount) = 0;
+
+        //! This is called for each entity in the lattice when it is being built. The subclass should attach
+        //! whatever components are necessary to achieve the desired result.
+        virtual void CreateLatticeInstance(const AZ::Transform& transform) = 0;
+
+        //! This is called after all the instances are created to any final work. Not required.
+        virtual void FinalizeLatticeInstances() {};
+
+        //! Called when the subclass should destroy all of its instances, either because of shutdown or recreation.
+        virtual void DestroyLatticeInstances() = 0;
+
+        void BuildLattice();
+
+        // These are signed to avoid casting with imgui controls.
+        int32_t m_latticeWidth = 5;
+        int32_t m_latticeHeight = 5;
+        int32_t m_latticeDepth = 5;
+        
+        Utils::DefaultIBL m_defaultIbl;
+    };
+} // namespace AtomSampleViewer

+ 45 - 0
Gem/Code/Source/EntityUtilityFunctions.cpp

@@ -0,0 +1,45 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <EntityUtilityFunctions.h>
+#include <AzFramework/Entity/EntityContextBus.h>
+
+namespace AtomSampleViewer
+{
+    AZ::Entity* CreateEntity(const AZStd::string_view& name, AzFramework::EntityContextId entityContextId)
+    {
+        AZ::Entity* entity = nullptr;
+        AzFramework::EntityContextRequestBus::EventResult(entity, entityContextId, &AzFramework::EntityContextRequestBus::Events::CreateEntity, name.data());
+        AZ_Assert(entity != nullptr, "Failed to create \"%s\" entity.", name.data());
+        return entity;
+    }
+
+    void DestroyEntity(AZ::Entity*& entity, AzFramework::EntityContextId entityContextId)
+    {
+        if (entity != nullptr)
+        {
+            AzFramework::EntityContextRequestBus::Event(entityContextId, &AzFramework::EntityContextRequestBus::Events::DestroyEntity, entity);
+            entity = nullptr;
+        }
+    }
+
+    void DestroyEntity(AZ::Entity*& entity)
+    {
+        if (entity != nullptr)
+        {
+            AzFramework::EntityContextId entityContextId = AzFramework::EntityContextId::CreateNull();
+            AzFramework::EntityIdContextQueryBus::EventResult(entityContextId, entity->GetId(), &AzFramework::EntityIdContextQueryBus::Events::GetOwningContextId);
+            DestroyEntity(entity, entityContextId);
+        }
+    }
+
+} //namespace AtomSampleViewer

+ 24 - 0
Gem/Code/Source/EntityUtilityFunctions.h

@@ -0,0 +1,24 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <AzCore/std/string/string_view.h>
+#include <AzCore/Component/Entity.h>
+#include <AzFramework/Entity/EntityContext.h>
+
+namespace AtomSampleViewer
+{
+    AZ::Entity* CreateEntity(const AZStd::string_view& name, AzFramework::EntityContextId entityContextId);
+    void DestroyEntity(AZ::Entity*& entity, AzFramework::EntityContextId entityContextId);
+    void DestroyEntity(AZ::Entity*& entity);
+}

+ 29 - 0
Gem/Code/Source/ExampleComponentBus.h

@@ -0,0 +1,29 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+#pragma once
+
+#include <AzCore/Component/ComponentBus.h>
+
+namespace AtomSampleViewer
+{
+    /**
+     * ExampleComponentRequestBus provides an interface to request operations on an example component
+     */
+    class ExampleComponentRequests
+        : public AZ::ComponentBus
+    {
+    public:
+        virtual void ResetCamera() = 0;
+    };
+    using ExampleComponentRequestBus = AZ::EBus<ExampleComponentRequests>;
+
+} // namespace AtomSampleViewer

+ 303 - 0
Gem/Code/Source/ExposureExampleComponent.cpp

@@ -0,0 +1,303 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <ExposureExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/CameraControllerBus.h>
+#include <Atom/Component/DebugCamera/NoClipControllerBus.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+#include <Atom/RPI.Public/Model/Model.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+
+#include <AzCore/Component/Entity.h>
+#include <AzFramework/Components/CameraBus.h>
+#include <AzFramework/Components/TransformComponent.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <EntityUtilityFunctions.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    void ExposureExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<ExposureExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    void ExposureExampleComponent::Activate()
+    {
+        using namespace AZ;
+
+        RPI::Scene* scene = RPI::RPISystemInterface::Get()->GetDefaultScene().get();
+        m_postProcessFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PostProcessFeatureProcessorInterface>();
+        m_pointLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PointLightFeatureProcessorInterface>();
+
+        EnableCameraController();
+
+        m_cameraTransformInitialized = false;
+        
+        SetupScene();
+
+        CreateExposureEntity();
+
+        m_imguiSidebar.Activate();
+
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void ExposureExampleComponent::Deactivate()
+    {
+        using namespace AZ;
+
+        AZ::TickBus::Handler::BusDisconnect();
+        DisableCameraController();
+
+        AZ::EntityBus::MultiHandler::BusDisconnect();
+
+        if (m_exposureControlSettings)
+        {
+            m_exposureControlSettings->SetEnabled(false);
+            m_exposureControlSettings->OnConfigChanged();
+        }
+
+        if (m_exposureEntity)
+        {
+            DestroyEntity(m_exposureEntity, GetEntityContextId());
+            m_postProcessFeatureProcessor = nullptr;
+        }
+
+        GetMeshFeatureProcessor()->ReleaseMesh(m_meshHandle);
+
+        m_pointLightFeatureProcessor->ReleaseLight(m_pointLight);
+        m_pointLightFeatureProcessor = nullptr;
+
+        m_imguiSidebar.Deactivate();
+
+    }
+
+    void ExposureExampleComponent::OnTick([[maybe_unused]] float deltaTime, AZ::ScriptTimePoint)
+    {
+        DrawSidebar();
+    }
+
+    void ExposureExampleComponent::OnEntityDestruction(const AZ::EntityId& entityId)
+    {
+        AZ::EntityBus::MultiHandler::BusDisconnect(entityId);
+
+        if (m_exposureEntity && m_exposureEntity->GetId() == entityId) 
+        {
+            m_postProcessFeatureProcessor->RemoveSettingsInterface(m_exposureEntity->GetId());
+            m_exposureEntity = nullptr;
+        }
+        else
+        {
+            AZ_Assert(false, "unexpected entity destruction is signaled.");
+        }
+    }
+
+    void ExposureExampleComponent::SetupScene()
+    {
+        using namespace AZ;
+
+        const char* bistroPath = "Objects/Bistro/Bistro_Research_Exterior.azmodel";
+        Data::Asset<RPI::ModelAsset> modelAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::ModelAsset>(bistroPath, RPI::AssetUtils::TraceLevel::Assert);
+        Data::Asset<RPI::MaterialAsset> materialAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::MaterialAsset>(DefaultPbrMaterialPath, RPI::AssetUtils::TraceLevel::Assert);
+        m_meshHandle = GetMeshFeatureProcessor()->AcquireMesh(modelAsset, AZ::RPI::Material::FindOrCreate(materialAsset));
+
+        // rotate the entity 180 degrees about Z (the vertical axis)
+        // This makes it consistent with how it was positioned in the world when the world was Y-up.
+        GetMeshFeatureProcessor()->SetTransform(m_meshHandle, Transform::CreateRotationZ(AZ::Constants::Pi));
+
+        Data::Instance<RPI::Model> model = GetMeshFeatureProcessor()->GetModel(m_meshHandle);
+        if (model)
+        {
+            OnModelReady(model);
+        }
+        else
+        {
+            GetMeshFeatureProcessor()->ConnectModelChangeEventHandler(m_meshHandle, m_meshChangedHandler);
+        }
+
+        SetupLights();
+    }
+
+    void ExposureExampleComponent::SetupLights()
+    {
+        auto lightHandle = m_pointLightFeatureProcessor->AcquireLight();
+
+        m_pointLightFeatureProcessor->SetPosition(lightHandle, AZ::Vector3(12.f, -12.6f, 4.3f));
+
+        AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Candela> color(40.0f * AZ::Color(1.f, 1.f, 1.f, 1.f));
+        m_pointLightFeatureProcessor->SetRgbIntensity(lightHandle, color);
+        m_pointLightFeatureProcessor->SetBulbRadius(lightHandle, 3.f);
+        m_pointLight = lightHandle;
+    }
+
+    void ExposureExampleComponent::OnModelReady(AZ::Data::Instance<AZ::RPI::Model> model)
+    {
+        m_bistroExteriorAssetLoaded = true;
+
+        SetInitialCameraTransform();
+    }
+
+    void ExposureExampleComponent::SetInitialCameraTransform()
+    {
+        using namespace AZ;
+
+        if (!m_cameraTransformInitialized)
+        {
+            const AZ::Vector3 InitPosition = AZ::Vector3(5.f, -10.f, 1.3f);
+            constexpr float InitPitch = 0.0f;
+            constexpr float InitHeading = -1.85f;
+            AZ::Transform cameraTrans = AZ::Transform::CreateIdentity();
+            cameraTrans.SetTranslation(InitPosition);
+            TransformBus::Event(
+                GetCameraEntityId(),
+                &TransformBus::Events::SetWorldTM,
+                cameraTrans);
+
+            AZ::Debug::NoClipControllerRequestBus::Event(
+                GetCameraEntityId(),
+                &AZ::Debug::NoClipControllerRequestBus::Events::SetPitch,
+                InitPitch);
+            AZ::Debug::NoClipControllerRequestBus::Event(
+                GetCameraEntityId(),
+                &AZ::Debug::NoClipControllerRequestBus::Events::SetHeading,
+                InitHeading);
+
+            m_cameraTransformInitialized = true;
+        }
+    }
+
+    void ExposureExampleComponent::EnableCameraController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+    }
+
+    void ExposureExampleComponent::DisableCameraController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+    }
+
+    void ExposureExampleComponent::CreateExposureEntity()
+    {
+        using namespace AZ;
+        m_exposureEntity = CreateEntity("Exposure", GetEntityContextId());
+
+        // Exposure
+        auto* exposureSettings = m_postProcessFeatureProcessor->GetOrCreateSettingsInterface(m_exposureEntity->GetId());
+        m_exposureControlSettings = exposureSettings->GetOrCreateExposureControlSettingsInterface();
+        m_exposureControlSettings->SetEnabled(true);
+        m_exposureControlSettings->SetExposureControlType(Render::ExposureControl::ExposureControlType::EyeAdaptation);
+
+        m_exposureEntity->Activate();
+        AZ::EntityBus::MultiHandler::BusConnect(m_exposureEntity->GetId());
+    }
+
+    void ExposureExampleComponent::DrawSidebar()
+    {
+        using namespace AZ::Render;
+
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        ImGui::Spacing();
+
+        ImGui::Text("Exposure");
+        ImGui::Indent();
+        {
+            bool exposureEnabled = m_exposureControlSettings->GetEnabled();
+            if (ImGui::Checkbox("Enabled Exposure", &exposureEnabled) || !m_isInitParameters)
+            {
+                m_exposureControlSettings->SetEnabled(exposureEnabled);
+                m_exposureControlSettings->OnConfigChanged();
+            }
+            ImGui::Spacing();
+
+            float manualCompensation = m_exposureControlSettings->GetManualCompensation();
+            if (ImGui::SliderFloat("Manual Compensation", &manualCompensation, -16.0f, 16.0f, "%0.4f") || !m_isInitParameters)
+            {
+                m_exposureControlSettings->SetManualCompensation(manualCompensation);
+                m_exposureControlSettings->OnConfigChanged();
+            }
+
+            if (ImGui::CollapsingHeader("EyeAdaptation", ImGuiTreeNodeFlags_DefaultOpen))
+            {
+
+                ImGui::Indent();
+                bool eyeAdaptationEnabled = m_exposureControlSettings->GetExposureControlType() == ExposureControl::ExposureControlType::EyeAdaptation;
+                if (ImGui::Checkbox("Enabled Eye Adaptation", &eyeAdaptationEnabled) || !m_isInitParameters)
+                {
+                    m_exposureControlSettings->SetExposureControlType(eyeAdaptationEnabled ? ExposureControl::ExposureControlType::EyeAdaptation
+                        : ExposureControl::ExposureControlType::ManualOnly);
+                    m_exposureControlSettings->OnConfigChanged();
+                }
+                ImGui::Spacing();
+
+
+                float minimumExposure = m_exposureControlSettings->GetEyeAdaptationExposureMin();
+                if (ImGui::SliderFloat("Minumum Exposure", &minimumExposure, -16.0f, 16.0f, "%0.4f") || !m_isInitParameters)
+                {
+                    m_exposureControlSettings->SetEyeAdaptationExposureMin(minimumExposure);
+                    m_exposureControlSettings->OnConfigChanged();
+                }
+
+                float maximumExposure = m_exposureControlSettings->GetEyeAdaptationExposureMax();
+                if (ImGui::SliderFloat("Maximum Exposure", &maximumExposure, -16.0f, 16.0f, "%0.4f") || !m_isInitParameters)
+                {
+                    m_exposureControlSettings->SetEyeAdaptationExposureMax(maximumExposure);
+                    m_exposureControlSettings->OnConfigChanged();
+                }
+
+                float speedUp = m_exposureControlSettings->GetEyeAdaptationSpeedUp();
+                if (ImGui::SliderFloat("Speed Up", &speedUp, 0.01, 10.0f, "%0.4f") || !m_isInitParameters)
+                {
+                    m_exposureControlSettings->SetEyeAdaptationSpeedUp(speedUp);
+                    m_exposureControlSettings->OnConfigChanged();
+                }
+
+                float speedDown = m_exposureControlSettings->GetEyeAdaptationSpeedDown();
+                if (ImGui::SliderFloat("Speed Down", &speedDown, 0.01, 10.0f, "%0.4f") || !m_isInitParameters)
+                {
+                    m_exposureControlSettings->SetEyeAdaptationSpeedDown(speedDown);
+                    m_exposureControlSettings->OnConfigChanged();
+                }
+
+                ImGui::Unindent();
+            }
+
+            m_isInitParameters = true;
+        }
+
+        ImGui::Unindent();
+
+        ImGui::Separator();
+
+        m_imguiSidebar.End();
+    }
+} // namespace AtomSampleViewer

+ 92 - 0
Gem/Code/Source/ExposureExampleComponent.h

@@ -0,0 +1,92 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <Atom/Feature/CoreLights/PointLightFeatureProcessorInterface.h>
+#include <Atom/Feature/PostProcess/PostProcessFeatureProcessorInterface.h>
+
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <Utils/Utils.h>
+#include <Utils/ImGuiSidebar.h>
+
+namespace AtomSampleViewer
+{
+    /*
+    * This component creates a simple scene to demonstrate the exposure feature.
+    */
+    class ExposureExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(ExposureExampleComponent, "{90D874F5-6DE8-4CF3-A6DC-754E903544FF}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        ExposureExampleComponent() = default;
+        ~ExposureExampleComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        using PointLightHandle = AZ::Render::PointLightFeatureProcessorInterface::LightHandle;
+        PointLightHandle m_pointLight;
+
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time);
+
+        // AZ::EntityBus::MultiHandler...
+        void OnEntityDestruction(const AZ::EntityId& entityId) override;
+
+        void SetInitialCameraTransform();
+        void EnableCameraController();
+        void DisableCameraController();
+
+        void CreateExposureEntity();
+        void SetupScene();
+        void SetupLights();
+        void OnModelReady(AZ::Data::Instance<AZ::RPI::Model> model);
+
+        void DrawSidebar();
+
+        // owning entity
+        AZ::Entity* m_exposureEntity = nullptr;
+        bool m_cameraTransformInitialized = false;
+        
+        // GUI
+        ImGuiSidebar m_imguiSidebar;
+
+        // initialize flag
+        bool m_isInitCamera = false;
+        bool m_isInitParameters = false;
+
+        // model
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+        AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler m_meshChangedHandler
+        {
+            [&](AZ::Data::Instance<AZ::RPI::Model> model) { OnModelReady(model); }
+        };
+        bool m_bistroExteriorAssetLoaded = false;
+
+        // feature processors
+        AZ::Render::PointLightFeatureProcessorInterface* m_pointLightFeatureProcessor = nullptr;
+        AZ::Render::PostProcessFeatureProcessorInterface* m_postProcessFeatureProcessor = nullptr;
+
+        AZ::Render::ExposureControlSettingsInterface* m_exposureControlSettings = nullptr;
+    };
+} // namespace AtomSampleViewer

+ 896 - 0
Gem/Code/Source/LightCullingExampleComponent.cpp

@@ -0,0 +1,896 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <LightCullingExampleComponent.h>
+#include <SampleComponentConfig.h>
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <Atom/Component/DebugCamera/CameraControllerBus.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+#include <imgui/imgui.h>
+
+#include <Atom/Feature/CoreLights/PhotometricValue.h>
+
+#include <Atom/Feature/CoreLights/CoreLightsConstants.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <AzCore/Asset/AssetManagerBus.h>
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Component/TransformBus.h>
+#include <AzCore/Math/Transform.h>
+#include <AzCore/Math/Obb.h>
+#include <AzFramework/Components/CameraBus.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Public/RenderPipeline.h>
+#include <Atom/RPI.Public/View.h>
+#include <Atom/RPI.Public/Pass/FullscreenTrianglePass.h>
+#include <Atom/RPI.Public/AuxGeom/AuxGeomFeatureProcessorInterface.h>
+#include <Atom/RPI.Public/AuxGeom/AuxGeomDraw.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+    using namespace AZ::Render;
+    using namespace AZ::RPI;
+
+    static const char* WorldModelName = "Objects/Bistro/Bistro_Research_Exterior.azmodel";
+
+    static const char* TransparentModelName = "Objects/ShaderBall_simple.azmodel";
+    static const char* TransparentMaterialName = "materials/DefaultPBRTransparent.azmaterial";
+
+    static const char* DecalMaterialPath = "materials/Decal/airship_tail_01_decal.azmaterial";
+
+    static const float TimingSmoothingFactor = 0.9f;
+    static const size_t MaxNumLights = 1024;
+    static const float AuxGeomDebugAlpha = 0.5f;
+    static const AZ::Vector3 CameraStartPositionVespa = AZ::Vector3(3.37f, -1.44f, 1.82f);
+    static const AZ::Vector3 CameraStartPositionLongViewDownStreet = AZ::Vector3(-12.f, -35.5f, 0.7438f);
+
+    AZ::Color LightCullingExampleComponent::GetRandomColor()
+    {
+        static const AZStd::vector<AZ::Color> colors = {
+            AZ::Colors::Red,
+            AZ::Colors::Blue,
+            AZ::Colors::Green,
+            AZ::Colors::White,
+            AZ::Colors::Purple,
+            AZ::Colors::MediumAquamarine,
+            AZ::Colors::Fuchsia,
+            AZ::Colors::Thistle,
+            AZ::Colors::LightGoldenrodYellow,
+            AZ::Colors::BlanchedAlmond,
+            AZ::Colors::PapayaWhip,
+            AZ::Colors::Bisque,
+            AZ::Colors::Chocolate,
+            AZ::Colors::MintCream,
+            AZ::Colors::LemonChiffon,
+            AZ::Colors::Plum
+        };
+
+        int randomNumber = m_random.GetRandom() % colors.size();
+        AZ::Color color = colors[randomNumber];
+        color.SetA(AuxGeomDebugAlpha);
+        return color;
+    }
+    static AZ::Render::MaterialAssignmentMap CreateMaterialAssignmentMap(const char* materialPath)
+    {
+        Render::MaterialAssignmentMap materials;
+        Render::MaterialAssignment& defaultMaterialAssignment = materials[AZ::Render::DefaultMaterialAssignmentId];
+        defaultMaterialAssignment.m_materialAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::MaterialAsset>(materialPath, RPI::AssetUtils::TraceLevel::Assert);
+        defaultMaterialAssignment.m_materialInstance = RPI::Material::FindOrCreate(defaultMaterialAssignment.m_materialAsset);
+        return materials;
+    }
+
+    LightCullingExampleComponent::LightCullingExampleComponent()
+    {
+        m_sampleName = "LightCullingExampleComponent";
+
+        // Add some initial lights to illuminate the scene
+        m_settings[(int)LightType::Point].m_numActive = 150;
+        m_settings[(int)LightType::Disk].m_intensity = 40.0f;
+        m_settings[(int)LightType::Capsule].m_intensity = 10.0f;
+    }
+
+    void LightCullingExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<LightCullingExampleComponent, AZ::Component>()
+                ->Version(0)
+            ;
+        }
+    }
+
+    void LightCullingExampleComponent::Activate()
+    {
+        GetFeatureProcessors();
+
+        // Don't continue the script until after the models have loaded and lights have been created. 
+        // Use a large timeout because of how slow this level loads in debug mode.
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScriptWithTimeout, 120.0f);
+
+        // preload assets
+        AZStd::vector<AssetCollectionAsyncLoader::AssetToLoadInfo> assetList = {
+            {WorldModelName, azrtti_typeid<RPI::ModelAsset>()},
+            {TransparentModelName, azrtti_typeid<RPI::ModelAsset>()},
+            {TransparentMaterialName, azrtti_typeid<RPI::MaterialAsset>()},
+            {DecalMaterialPath, azrtti_typeid<RPI::MaterialAsset>()}
+        };
+
+        PreloadAssets(assetList);
+    }
+
+    void LightCullingExampleComponent::OnAllAssetsReadyActivate()
+    {
+        LoadDecalMaterial();
+        SetupScene();
+        SetupCamera();
+
+        m_imguiSidebar.Activate();
+
+        AZ::TickBus::Handler::BusConnect();
+
+        // Now that the model and all the lights are initialized, we can allow the script to continue.
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+    }
+
+    void LightCullingExampleComponent::SetupCamera()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+        SaveCameraConfiguration();
+
+        const float FarClipDistance = 16384.0f;
+
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFarClipDistance,
+            FarClipDistance);
+    }
+
+    void LightCullingExampleComponent::Deactivate()
+    {
+        m_decalMaterial = {};
+        DisableHeatmap();
+
+        AZ::TickBus::Handler::BusDisconnect();
+
+        m_imguiSidebar.Deactivate();
+
+        RestoreCameraConfiguration();
+        AZ::Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+
+        DestroyOpaqueModels();
+        DestroyTransparentModels();
+
+        DestroyLightsAndDecals();
+    }
+
+    void LightCullingExampleComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        if (m_worldModelAssetLoaded)
+        {
+            CalculateSmoothedFPS(deltaTime);
+            DrawSidebar();
+            DrawDebuggingHelpers();
+            UpdateLights();
+        }
+    }
+
+    void LightCullingExampleComponent::UpdateLights()
+    {
+        if (m_refreshLights)
+        {
+            DestroyLightsAndDecals();
+
+            CreateLightsAndDecals();
+
+
+            m_refreshLights = false;
+        }
+    }
+
+
+    void LightCullingExampleComponent::OnModelReady(AZ::Data::Instance<AZ::RPI::Model> model)
+    {
+        m_meshChangedHandler.Disconnect();
+        m_worldModelAssetLoaded = true;
+        m_worldModelAABB = model->GetAabb();
+
+        InitLightArrays();
+        CreateLightsAndDecals();
+        MoveCameraToStartPosition();
+    }
+
+    void LightCullingExampleComponent::SaveCameraConfiguration()
+    {
+        Camera::CameraRequestBus::EventResult(
+            m_originalFarClipDistance,
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::GetFarClipDistance);
+    }
+
+    void LightCullingExampleComponent::RestoreCameraConfiguration()
+    {
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFarClipDistance,
+            m_originalFarClipDistance);
+    }
+
+    void LightCullingExampleComponent::SetupScene()
+    {
+        using namespace AZ;
+        CreateTransparentModels();
+        CreateOpaqueModels();
+    }
+
+    AZ::Vector3 LightCullingExampleComponent::GetRandomPositionInsideWorldModel()
+    {
+        AZ::Vector3 randomPosition;
+
+        float x = m_random.GetRandomFloat() * m_worldModelAABB.GetXExtent();
+        float y = m_random.GetRandomFloat() * m_worldModelAABB.GetYExtent();
+        float z = m_random.GetRandomFloat() * m_worldModelAABB.GetZExtent();
+
+        randomPosition.SetX(x);
+        randomPosition.SetY(y);
+        randomPosition.SetZ(z);
+
+        randomPosition += m_worldModelAABB.GetMin();
+        return randomPosition;
+    }
+
+    AZ::Vector3 LightCullingExampleComponent::GetRandomDirection()
+    {
+        float x = m_random.GetRandomFloat() - 0.5f;
+        float y = m_random.GetRandomFloat() - 0.5f;
+        float z = m_random.GetRandomFloat() - 0.5f;
+        AZ::Vector3 direction(x, y, z);
+        direction.NormalizeSafe();
+        return direction;
+    }
+
+    float LightCullingExampleComponent::GetRandomNumber(float low, float high)
+    {
+        float r = m_random.GetRandomFloat();
+        return r * (high - low) + low;
+    }
+
+    void LightCullingExampleComponent::CreatePointLights()
+    {
+        for (int i = 0; i < m_settings[(int)LightType::Point].m_numActive; ++i)
+        {
+            CreatePointLight(i);
+        }
+    }
+
+    void LightCullingExampleComponent::CreateSpotLights()
+    {
+        for (int i = 0; i < m_settings[(int)LightType::Spot].m_numActive; ++i)
+        {
+            CreateSpotLight(i);
+        }
+    }
+
+    void LightCullingExampleComponent::CreateDiskLights()
+    {
+        for (int i = 0; i < m_settings[(int)LightType::Disk].m_numActive; ++i)
+        {
+            CreateDiskLight(i);
+        }
+    }
+
+    void LightCullingExampleComponent::CreateCapsuleLights()
+    {
+        for (int i = 0; i < m_settings[(int)LightType::Capsule].m_numActive; ++i)
+        {
+            CreateCapsuleLight(i);
+        }
+    }
+
+    void LightCullingExampleComponent::CreateQuadLights()
+    {
+        for (int i = 0; i < m_settings[(int)LightType::Quad].m_numActive; ++i)
+        {
+            CreateQuadLight(i);
+        }
+    }
+
+    void LightCullingExampleComponent::CreateDecals()
+    {
+        for (int i = 0; i < m_settings[(int)LightType::Decal].m_numActive; ++i)
+        {
+            CreateDecal(i);
+        }
+    }
+
+
+    void LightCullingExampleComponent::DestroyDecals()
+    {
+        for (size_t i = 0; i < m_decals.size(); ++i)
+        {
+            m_decalFeatureProcessor->ReleaseDecal(m_decals[i].m_decalHandle);
+            m_decals[i].m_decalHandle = DecalHandle::Null;
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebar()
+    {
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        if (!m_worldModelAssetLoaded)
+        {
+            const ImGuiWindowFlags windowFlags =
+                ImGuiWindowFlags_NoCollapse |
+                ImGuiWindowFlags_NoResize |
+                ImGuiWindowFlags_NoMove;
+
+            if (ImGui::Begin("Asset", nullptr, windowFlags))
+            {
+                ImGui::Text("World Model: %s", m_worldModelAssetLoaded ? "Loaded" : "Loading...");
+
+                ImGui::End();
+            }
+        }
+
+        DrawSidebarTimingSection();
+
+        ImGui::Spacing();
+        ImGui::Separator();
+
+        DrawSidebarPointLightsSection(&m_settings[(int)LightType::Point]);
+        DrawSidebarSpotLightsSection(&m_settings[(int)LightType::Spot]);
+        DrawSidebarDiskLightsSection(&m_settings[(int)LightType::Disk]);
+        DrawSidebarCapsuleLightSection(&m_settings[(int)LightType::Capsule]);
+        DrawSidebarQuadLightsSections(&m_settings[(int)LightType::Quad]);
+        DrawSidebarDecalSection(&m_settings[(int)LightType::Decal]);
+        DrawSidebarHeatmapOpacity();
+
+        m_imguiSidebar.End();
+    }
+
+    void LightCullingExampleComponent::DrawSidebarPointLightsSection(LightSettings* lightSettings)
+    {
+        ScriptableImGui::ScopedNameContext context{ "Point Lights" };
+        if (ImGui::CollapsingHeader("Point Lights", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed))
+        {
+            m_refreshLights |= ScriptableImGui::SliderInt("Point light count", &lightSettings->m_numActive, 0, MaxNumLights);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Bulb Radius", &m_bulbRadius, 0.0f, 20.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Point Intensity", &lightSettings->m_intensity, 0.0f, 200.0f);
+            m_refreshLights |= ScriptableImGui::Checkbox("Enable automatic light falloff (Point)", &lightSettings->m_enableAutomaticFalloff);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Point Attenuation Radius", &lightSettings->m_attenuationRadius, 0.0f, 20.0f);
+            ScriptableImGui::Checkbox("Draw Debug Spheres", &lightSettings->m_enableDebugDraws);
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebarSpotLightsSection(LightSettings* lightSettings)
+    {
+        ScriptableImGui::ScopedNameContext context{"Spot Lights"};
+        if (ImGui::CollapsingHeader("Spot Lights", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed))
+        {
+            m_refreshLights |= ScriptableImGui::SliderInt("Spot light count", &lightSettings->m_numActive, 0, MaxNumLights);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Spot Intensity", &lightSettings->m_intensity, 0.0f, 200.0f);
+            m_refreshLights |= ScriptableImGui::Checkbox("Enable automatic light falloff (Spot)", &lightSettings->m_enableAutomaticFalloff);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Spot Attenuation Radius", &lightSettings->m_attenuationRadius, 0.0f, 20.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Inner Cone (degrees)", &m_spotInnerConeDegrees, 0.0f, 180.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Outer Cone (degrees)", &m_spotOuterConeDegrees, 0.0f, 180.0f);
+            ScriptableImGui::Checkbox("Draw Debug Cones", &lightSettings->m_enableDebugDraws);
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebarDiskLightsSection(LightSettings* lightSettings)
+    {
+        ScriptableImGui::ScopedNameContext context{"Disk Lights"};
+        if (ImGui::CollapsingHeader("Disk Lights", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed))
+        {
+            m_refreshLights |= ScriptableImGui::SliderInt("Disk light count", &lightSettings->m_numActive, 0, MaxNumLights);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Disk Radius", &m_diskRadius, 0.0f, 20.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Disk Attenuation Radius", &lightSettings->m_attenuationRadius, 0.0f, 20.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Disk Intensity", &lightSettings->m_intensity, 0.0f, 200.0f);
+            m_refreshLights |= ScriptableImGui::Checkbox("Double sided disk", &m_isDiskDoubleSided);
+            ScriptableImGui::Checkbox("Draw disk lights", &lightSettings->m_enableDebugDraws);
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebarCapsuleLightSection(LightSettings* lightSettings)
+    {
+        ScriptableImGui::ScopedNameContext context{"Capsule Lights"};
+        if (ImGui::CollapsingHeader("Capsule Lights", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed))
+        {
+            m_refreshLights |= ScriptableImGui::SliderInt("Capsule light count", &lightSettings->m_numActive, 0, MaxNumLights);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Capsule Intensity", &lightSettings->m_intensity, 0.0f, 200.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Capsule Radius", &m_capsuleRadius, 0.0f, 5.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Capsule Length", &m_capsuleLength, 0.0f, 20.0f);
+            ScriptableImGui::Checkbox("Draw capsule lights", &lightSettings->m_enableDebugDraws);
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebarQuadLightsSections(LightSettings* lightSettings)
+    {
+        ScriptableImGui::ScopedNameContext context{ "Quad Lights" };
+        if (ImGui::CollapsingHeader("Quad Lights", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed))
+        {
+            m_refreshLights |= ScriptableImGui::SliderInt("Quad light count", &lightSettings->m_numActive, 0, MaxNumLights);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Quad Attenuation Radius", &lightSettings->m_attenuationRadius, 0.0f, 20.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Quad light width", &m_quadLightSize[0], 0.0f, 10.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Quad light height", &m_quadLightSize[1], 0.0f, 10.0f);
+            m_refreshLights |= ScriptableImGui::Checkbox("Double sided quad", &m_isQuadLightDoubleSided);
+            m_refreshLights |= ScriptableImGui::Checkbox("Use fast approximation", &m_quadLightsUseFastApproximation);
+            ScriptableImGui::Checkbox("Draw quad lights", &lightSettings->m_enableDebugDraws);
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebarHeatmapOpacity()
+    {
+        ScriptableImGui::ScopedNameContext context{"Heatmap"};
+        ImGui::Text("Heatmap");
+        ImGui::Indent();
+        bool opacityChanged = ScriptableImGui::SliderFloat("Opacity", &m_heatmapOpacity, 0, 1);
+        ImGui::Unindent();
+
+        if (opacityChanged)
+        {
+            UpdateHeatmapOpacity();
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebarDecalSection(LightSettings* lightSettings)
+    {
+        ScriptableImGui::ScopedNameContext context{"Decals"};
+        if (ImGui::CollapsingHeader("Decals", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed))
+        {
+            m_refreshLights |= ScriptableImGui::SliderInt("Decal count", &lightSettings->m_numActive, 0, MaxNumLights);
+            ScriptableImGui::Checkbox("Draw decals", &lightSettings->m_enableDebugDraws);
+            m_refreshLights |= ScriptableImGui::SliderFloat3("Decal Size", m_decalSize.data(), 0.0f, 10.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Decal Opacity", &m_decalOpacity, 0.0f, 1.0f);
+            m_refreshLights |= ScriptableImGui::SliderFloat("Decal Angle Attenuation", &m_decalAngleAttenuation, 0.0f, 1.0f);
+        }
+    }
+
+    void LightCullingExampleComponent::CreatePointLight(int index)
+    {
+        auto& light = m_pointLights[index];
+        AZ_Assert(light.m_lightHandle.IsNull(), "CreatePointLight called on a light that was already created previously");
+
+        light.m_lightHandle = m_pointLightFeatureProcessor->AcquireLight();
+
+        const LightSettings& settings = m_settings[(int)LightType::Point];
+
+        m_pointLightFeatureProcessor->SetPosition(light.m_lightHandle, light.m_position);
+        m_pointLightFeatureProcessor->SetRgbIntensity(light.m_lightHandle, PhotometricColor<PhotometricUnit::Candela>(settings.m_intensity * light.m_color));
+        m_pointLightFeatureProcessor->SetBulbRadius(light.m_lightHandle, m_bulbRadius);
+
+        float attenuationRadius = settings.m_enableAutomaticFalloff ? AutoCalculateAttenuationRadius(light.m_color, settings.m_intensity) : settings.m_attenuationRadius;
+        m_pointLightFeatureProcessor->SetAttenuationRadius(light.m_lightHandle, attenuationRadius);
+    }
+
+    void LightCullingExampleComponent::CreateDiskLight(int index)
+    {
+        auto& light = m_diskLights[index];
+        light.m_lightHandle = m_diskLightFeatureProcessor->AcquireLight();
+
+        const LightSettings& settings = m_settings[(int)LightType::Disk];
+
+        m_diskLightFeatureProcessor->SetDiskRadius(light.m_lightHandle, m_diskRadius);
+        m_diskLightFeatureProcessor->SetPosition(light.m_lightHandle, light.m_position);
+        m_diskLightFeatureProcessor->SetRgbIntensity(light.m_lightHandle, PhotometricColor<PhotometricUnit::Candela>(settings.m_intensity * light.m_color));
+        m_diskLightFeatureProcessor->SetDirection(light.m_lightHandle, light.m_direction);
+        m_diskLightFeatureProcessor->SetLightEmitsBothDirections(light.m_lightHandle, m_isDiskDoubleSided);
+
+        float attenuationRadius = settings.m_enableAutomaticFalloff ? AutoCalculateAttenuationRadius(light.m_color, settings.m_intensity) : settings.m_attenuationRadius;
+        m_diskLightFeatureProcessor->SetAttenuationRadius(light.m_lightHandle, m_settings[(int)LightType::Disk].m_attenuationRadius);
+    }
+
+    void LightCullingExampleComponent::CreateCapsuleLight(int index)
+    {
+        auto& light = m_capsuleLights[index];
+        AZ_Assert(light.m_lightHandle.IsNull(), "CreateCapsuleLight called on a light that was already created previously");
+        light.m_lightHandle = m_capsuleLightFeatureProcessor->AcquireLight();
+
+        const LightSettings& settings = m_settings[(int)LightType::Capsule];
+
+        m_capsuleLightFeatureProcessor->SetAttenuationRadius(light.m_lightHandle, m_settings[(int)LightType::Capsule].m_attenuationRadius);
+        m_capsuleLightFeatureProcessor->SetRgbIntensity(light.m_lightHandle, PhotometricColor<PhotometricUnit::Candela>(settings.m_intensity * light.m_color));
+        m_capsuleLightFeatureProcessor->SetCapsuleRadius(light.m_lightHandle, m_capsuleRadius);
+
+        AZ::Vector3 startPoint = light.m_position - light.m_direction * m_capsuleLength * 0.5f;
+        AZ::Vector3 endPoint = light.m_position + light.m_direction * m_capsuleLength * 0.5f;
+        m_capsuleLightFeatureProcessor->SetCapsuleLineSegment(light.m_lightHandle, startPoint, endPoint);
+    }
+
+    void LightCullingExampleComponent::CreateQuadLight(int index)
+    {
+        auto& light = m_quadLights[index];
+        AZ_Assert(light.m_lightHandle.IsNull(), "CreateQuadLight called on a light that was already created previously");
+        light.m_lightHandle = m_quadLightFeatureProcessor->AcquireLight();
+
+        const LightSettings& settings = m_settings[(int)LightType::Quad];
+
+        m_quadLightFeatureProcessor->SetRgbIntensity(light.m_lightHandle, PhotometricColor<PhotometricUnit::Nit>(settings.m_intensity * light.m_color));
+
+        const auto orientation = AZ::Quaternion::CreateFromVector3(light.m_direction);
+        m_quadLightFeatureProcessor->SetOrientation(light.m_lightHandle, orientation);
+        m_quadLightFeatureProcessor->SetQuadDimensions(light.m_lightHandle, m_quadLightSize[0], m_quadLightSize[1]);
+        m_quadLightFeatureProcessor->SetLightEmitsBothDirections(light.m_lightHandle, m_isQuadLightDoubleSided);
+        m_quadLightFeatureProcessor->SetUseFastApproximation(light.m_lightHandle, m_quadLightsUseFastApproximation);
+        m_quadLightFeatureProcessor->SetAttenuationRadius(light.m_lightHandle, m_settings[(int)LightType::Quad].m_attenuationRadius);
+        m_quadLightFeatureProcessor->SetPosition(light.m_lightHandle, light.m_position);
+    }
+
+    void LightCullingExampleComponent::CreateSpotLight(int index)
+    {
+        auto& light = m_spotLights[index];
+        light.m_lightHandle = m_spotLightFeatureProcessor->AcquireLight();
+        const LightSettings& settings = m_settings[(int)LightType::Spot];
+
+        m_spotLightFeatureProcessor->SetPosition(light.m_lightHandle, light.m_position);
+        m_spotLightFeatureProcessor->SetDirection(light.m_lightHandle, light.m_direction);
+        m_spotLightFeatureProcessor->SetRgbIntensity(light.m_lightHandle, PhotometricColor<PhotometricUnit::Candela>(settings.m_intensity * light.m_color));
+
+        float attenuationRadius = settings.m_enableAutomaticFalloff ? AutoCalculateAttenuationRadius(light.m_color, settings.m_intensity) : settings.m_attenuationRadius;
+        m_spotLightFeatureProcessor->SetAttenuationRadius(light.m_lightHandle, attenuationRadius);
+        m_spotLightFeatureProcessor->SetConeAngles(light.m_lightHandle, m_spotInnerConeDegrees, m_spotOuterConeDegrees);
+    }
+
+    void LightCullingExampleComponent::CreateDecal(int index)
+    {
+        Decal& decal = m_decals[index];
+        decal.m_decalHandle = m_decalFeatureProcessor->AcquireDecal();
+
+        AZ::Render::DecalData decalData;
+        decalData.m_position = {
+            { decal.m_position.GetX(), decal.m_position.GetY(), decal.m_position.GetZ() }
+        };
+        decalData.m_halfSize = {
+            {m_decalSize[0] * 0.5f, m_decalSize[1] * 0.5f, m_decalSize[2] * 0.5f}
+        };
+        decalData.m_quaternion = {
+            { decal.m_quaternion.GetX(), decal.m_quaternion.GetY(), decal.m_quaternion.GetZ(), decal.m_quaternion.GetW() }
+        };
+        decalData.m_angleAttenuation = m_decalAngleAttenuation;
+        decalData.m_opacity = m_decalOpacity;
+
+        m_decalFeatureProcessor->SetDecalData(decal.m_decalHandle, decalData);
+
+        m_decalFeatureProcessor->SetDecalMaterial(decal.m_decalHandle, m_decalMaterial.GetId());
+
+    }
+
+    void LightCullingExampleComponent::DrawPointLightDebugSpheres(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        const LightSettings& settings = m_settings[(int)LightType::Point];
+        int numToDraw = AZStd::min(m_settings[(int)LightType::Point].m_numActive, aznumeric_cast<int>(m_pointLights.size()));
+        for (int i = 0; i < numToDraw; ++i)
+        {
+            const auto& light = m_pointLights[i];
+            if (light.m_lightHandle.IsNull())
+            {
+                continue;
+            }
+
+            float radius = settings.m_enableAutomaticFalloff ? AutoCalculateAttenuationRadius(light.m_color, settings.m_intensity) : settings.m_attenuationRadius;
+            auxGeom->DrawSphere(light.m_position, radius, light.m_color, AZ::RPI::AuxGeomDraw::DrawStyle::Shaded);
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSpotLightDebugCones(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        const LightSettings& settings = m_settings[(int)LightType::Spot];
+        int numToDraw = AZStd::min(settings.m_numActive, aznumeric_cast<int>(m_spotLights.size()));
+        for (int i = 0; i < numToDraw; ++i)
+        {
+            const auto& light = m_spotLights[i];
+            if (light.m_lightHandle.IsNull())
+            {
+                continue;
+            }
+
+            float height = settings.m_enableAutomaticFalloff ? AutoCalculateAttenuationRadius(light.m_color, settings.m_intensity) : settings.m_attenuationRadius;
+            float angleRadians = AZ::DegToRad(m_spotOuterConeDegrees * 0.5f);
+            float radius = tanf(angleRadians) * height;
+            auxGeom->DrawCone(light.m_position + light.m_direction * height, -light.m_direction, radius, height, light.m_color, AZ::RPI::AuxGeomDraw::DrawStyle::Shaded);
+        }
+    }
+
+    void LightCullingExampleComponent::DrawDecalDebugBoxes(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        int numToDraw = AZStd::min(m_settings[(int)LightType::Decal].m_numActive, aznumeric_cast<int>(m_decals.size()));
+        for (int i = 0; i < numToDraw; ++i)
+        {
+            const Decal& decal = m_decals[i];
+            if (decal.m_decalHandle.IsValid())
+            {
+                AZ::Aabb aabb = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3::CreateFromFloat3(m_decalSize.data()) * 0.5f);
+                AZ::Matrix3x4 transform = AZ::Matrix3x4::CreateFromQuaternionAndTranslation(decal.m_quaternion, decal.m_position);
+                auxGeom->DrawObb(AZ::Obb::CreateFromAabb(aabb), transform, AZ::Colors::White, AZ::RPI::AuxGeomDraw::DrawStyle::Line);
+            }
+        }
+    }
+
+    void LightCullingExampleComponent::DrawDiskLightDebugObjects(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        int numToDraw = AZStd::min(m_settings[(int)LightType::Disk].m_numActive, aznumeric_cast<int>(m_diskLights.size()));
+        for (int i = 0; i < numToDraw; ++i)
+        {
+            const auto& light = m_diskLights[i];
+            if (light.m_lightHandle.IsValid())
+            {
+                auxGeom->DrawDisk(light.m_position, light.m_direction, m_diskRadius, light.m_color, AZ::RPI::AuxGeomDraw::DrawStyle::Shaded);
+            }
+        }
+    }
+
+    void LightCullingExampleComponent::DrawCapsuleLightDebugObjects(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        int numToDraw = AZStd::min(m_settings[(int)LightType::Capsule].m_numActive, aznumeric_cast<int>(m_capsuleLights.size()));
+        for (int i = 0; i < numToDraw; ++i)
+        {
+            const auto& light = m_capsuleLights[i];
+            if (light.m_lightHandle.IsValid())
+            {
+                auxGeom->DrawCylinder(light.m_position, light.m_direction, m_capsuleRadius, m_capsuleLength, light.m_color);
+            }
+        }
+    }
+
+    void LightCullingExampleComponent::DrawQuadLightDebugObjects(AZ::RPI::AuxGeomDrawPtr auxGeom)
+    {
+        int numToDraw = AZStd::min(m_settings[(int)LightType::Quad].m_numActive, aznumeric_cast<int>(m_quadLights.size()));
+        for (int i = 0; i < numToDraw; ++i)
+        {
+            const auto& light = m_quadLights[i];
+            if (light.m_lightHandle.IsValid())
+            {
+                auto transform = AZ::Transform::CreateFromQuaternionAndTranslation(AZ::Quaternion::CreateFromVector3(light.m_direction), light.m_position);
+
+                // Rotate 90 degrees so that the debug draw is aligned properly with the quad light
+                transform *= AZ::Transform::CreateFromQuaternion(AZ::ConvertEulerRadiansToQuaternion(AZ::Vector3(AZ::Constants::HalfPi, 0.0f, 0.0f)));
+                auxGeom->DrawQuad(m_quadLightSize[0], m_quadLightSize[1], transform, light.m_color);
+            }
+        }
+    }
+
+    void LightCullingExampleComponent::DrawDebuggingHelpers()
+    {
+        auto defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        if (auto auxGeom = AZ::RPI::AuxGeomFeatureProcessorInterface::GetDrawQueueForScene(defaultScene))
+        {
+            if (m_settings[(int)LightType::Point].m_enableDebugDraws)
+            {
+                DrawPointLightDebugSpheres(auxGeom);
+            }
+            if (m_settings[(int)LightType::Spot].m_enableDebugDraws)
+            {
+                DrawSpotLightDebugCones(auxGeom);
+            }
+            if (m_settings[(int)LightType::Disk].m_enableDebugDraws)
+            {
+                DrawDiskLightDebugObjects(auxGeom);
+            }
+            if (m_settings[(int)LightType::Capsule].m_enableDebugDraws)
+            {
+                DrawCapsuleLightDebugObjects(auxGeom);
+            }
+            if (m_settings[(int)LightType::Quad].m_enableDebugDraws)
+            {
+                DrawQuadLightDebugObjects(auxGeom);
+            }
+            if (m_settings[(int)LightType::Decal].m_enableDebugDraws)
+            {
+                DrawDecalDebugBoxes(auxGeom);
+            }
+        }
+    }
+
+    void LightCullingExampleComponent::DrawSidebarTimingSection()
+    {
+        ImGui::Text("Timing");
+        DrawSidebarTimingSectionCPU();
+    }
+
+    void LightCullingExampleComponent::CalculateSmoothedFPS(float deltaTimeSeconds)
+    {
+        float currFPS = 1.0f / deltaTimeSeconds;
+        m_smoothedFPS = TimingSmoothingFactor * m_smoothedFPS + (1.0f - TimingSmoothingFactor) * currFPS;
+    }
+
+    void LightCullingExampleComponent::DrawSidebarTimingSectionCPU()
+    {
+        ImGui::Text("CPU (ms)");
+        ImGui::Indent();
+        ImGui::Text("Total: %5.1f", 1000.0f / m_smoothedFPS);
+        ImGui::Unindent();
+    }
+
+    void LightCullingExampleComponent::InitLightArrays()
+    {
+        // Set seed to the same value each time so values are consistent between multiple app runs.
+        // Intended for use with the screenshot comparison tool
+        m_random.SetSeed(0);
+
+        const auto InitLight = [this](auto& light)
+            {
+                light.m_color = GetRandomColor();
+                light.m_position = GetRandomPositionInsideWorldModel();
+                light.m_direction = GetRandomDirection();
+            };
+
+        m_pointLights.resize(MaxNumLights);
+        AZStd::for_each(m_pointLights.begin(), m_pointLights.end(), InitLight);
+
+        m_spotLights.resize(MaxNumLights);
+        AZStd::for_each(m_spotLights.begin(), m_spotLights.end(), InitLight);
+
+        m_diskLights.resize(MaxNumLights);
+        AZStd::for_each(m_diskLights.begin(), m_diskLights.end(), InitLight);
+
+        m_capsuleLights.resize(MaxNumLights);
+        AZStd::for_each(m_capsuleLights.begin(), m_capsuleLights.end(), InitLight);
+        m_decals.resize(MaxNumLights);
+        AZStd::for_each(m_decals.begin(), m_decals.end(), [&](Decal& decal)
+            {
+                decal.m_position = GetRandomPositionInsideWorldModel();
+                decal.m_quaternion = AZ::Quaternion::CreateFromAxisAngle(GetRandomDirection(), GetRandomNumber(0.0f, AZ::Constants::TwoPi));
+            });
+
+        m_quadLights.resize(MaxNumLights);
+        AZStd::for_each(m_quadLights.begin(), m_quadLights.end(), InitLight);
+    }
+
+    void LightCullingExampleComponent::GetFeatureProcessors()
+    {
+        AZ::RPI::Scene* scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get();
+        m_pointLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::PointLightFeatureProcessorInterface>();
+        m_spotLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::SpotLightFeatureProcessorInterface>();
+        m_diskLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DiskLightFeatureProcessorInterface>();
+        m_capsuleLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::CapsuleLightFeatureProcessorInterface>();
+        m_quadLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::QuadLightFeatureProcessorInterface>();
+        m_decalFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DecalFeatureProcessorInterface>();
+    }
+
+    float LightCullingExampleComponent::AutoCalculateAttenuationRadius(const AZ::Color& color, float intensity)
+    {
+        // Get combined intensity luma from m_photometricValue, then calculate the radius at which the irradiance will be equal to cutoffIntensity.
+        static const float CutoffIntensity = 0.1f; // Make this configurable later.
+
+        float luminance = AZ::Render::PhotometricValue::GetPerceptualLuminance(color * intensity);
+        return sqrt(luminance / CutoffIntensity);
+    }
+
+    void LightCullingExampleComponent::MoveCameraToStartPosition()
+    {
+        const AZ::Vector3 target = AZ::Vector3::CreateAxisZ();
+        const AZ::Transform transform = AZ::Transform::CreateLookAt(CameraStartPositionLongViewDownStreet, target, AZ::Transform::Axis::YPositive);
+        AZ::TransformBus::Event(GetCameraEntityId(), &AZ::TransformBus::Events::SetWorldTM, transform);
+    }
+
+    void LightCullingExampleComponent::UpdateHeatmapOpacity()
+    {
+        if (const ScenePtr scene = RPISystemInterface::Get()->GetDefaultScene())
+        {
+            if (const RenderPipelinePtr pipeline = scene->GetDefaultRenderPipeline())
+            {
+                if (const Ptr<Pass> pass = pipeline->GetRootPass()->FindPassByNameRecursive(AZ::Name("LightCullingHeatmapPass")))
+                {
+                    if (const Ptr<RenderPass> trianglePass = azrtti_cast<RenderPass*>(pass))
+                    {
+                        trianglePass->SetEnabled(m_heatmapOpacity > 0.0f);
+                        Data::Instance<ShaderResourceGroup> srg = trianglePass->GetShaderResourceGroup();
+                        RHI::ShaderInputConstantIndex opacityIndex = srg->FindShaderInputConstantIndex(AZ::Name("m_heatmapOpacity"));
+                        bool setOk = srg->SetConstant<float>(opacityIndex, m_heatmapOpacity);
+                        AZ_Warning("LightCullingExampleComponent", setOk, "Unable to set heatmap opacity");
+                    }
+                }
+            }
+        }
+    }
+
+    void LightCullingExampleComponent::DisableHeatmap()
+    {
+        m_heatmapOpacity = 0.0f;
+        UpdateHeatmapOpacity();
+    }
+
+    void LightCullingExampleComponent::CreateOpaqueModels()
+    {
+        Data::Asset<RPI::ModelAsset> modelAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::ModelAsset>(WorldModelName, RPI::AssetUtils::TraceLevel::Assert);
+
+        auto meshFeatureProcessor = GetMeshFeatureProcessor();
+
+        m_meshHandle = meshFeatureProcessor->AcquireMesh(modelAsset);
+        meshFeatureProcessor->SetTransform(m_meshHandle, Transform::CreateIdentity());
+        Data::Instance<RPI::Model> model = meshFeatureProcessor->GetModel(m_meshHandle);
+        // Loading in the world will probably take a while and I want to grab the AABB afterwards, so hook it up to a a ModelChangeEventHandler
+        if (model)
+        {
+            OnModelReady(model);
+        }
+        else
+        {
+            meshFeatureProcessor->ConnectModelChangeEventHandler(m_meshHandle, m_meshChangedHandler);
+        }
+    }
+
+    void LightCullingExampleComponent::CreateTransparentModels()
+    {
+        const AZStd::vector<AZ::Vector3 > TransparentModelPositions = { AZ::Vector3(-6.f, -20, 1),
+                                                                        AZ::Vector3(7.5f, 0, 1)
+        };
+
+        Data::Asset<RPI::ModelAsset> transparentModelAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::ModelAsset>(TransparentModelName, RPI::AssetUtils::TraceLevel::Assert);
+
+        // Override the shader ball material with a transparent material
+        Render::MaterialAssignmentMap materialAssignmentMap = CreateMaterialAssignmentMap(TransparentMaterialName);
+        for (const AZ::Vector3& position : TransparentModelPositions)
+        {
+            AZ::Render::MeshFeatureProcessorInterface::MeshHandle meshHandle = GetMeshFeatureProcessor()->AcquireMesh(transparentModelAsset, materialAssignmentMap);
+            GetMeshFeatureProcessor()->SetTransform(meshHandle, Transform::CreateTranslation(position));
+            m_transparentMeshHandles.push_back(std::move(meshHandle));
+        }
+    }
+
+    void LightCullingExampleComponent::DestroyOpaqueModels()
+    {
+        GetMeshFeatureProcessor()->ReleaseMesh(m_meshHandle);
+    }
+
+    void LightCullingExampleComponent::DestroyTransparentModels()
+    {
+        for (auto& elem : m_transparentMeshHandles)
+        {
+            GetMeshFeatureProcessor()->ReleaseMesh(elem);
+        }
+        m_transparentMeshHandles.clear();
+    }
+
+    void LightCullingExampleComponent::DestroyLightsAndDecals()
+    {
+        DestroyLights(m_pointLightFeatureProcessor, m_pointLights);
+        DestroyLights(m_spotLightFeatureProcessor, m_spotLights);
+        DestroyLights(m_diskLightFeatureProcessor, m_diskLights);
+        DestroyLights(m_capsuleLightFeatureProcessor, m_capsuleLights);
+        DestroyLights(m_quadLightFeatureProcessor, m_quadLights);
+        DestroyDecals();
+    }
+
+    void LightCullingExampleComponent::CreateLightsAndDecals()
+    {
+        CreatePointLights();
+        CreateSpotLights();
+        CreateDiskLights();
+        CreateCapsuleLights();
+        CreateQuadLights();
+        CreateDecals();
+    }
+
+    void LightCullingExampleComponent::LoadDecalMaterial()
+    {
+        const AZ::Data::AssetId id = AZ::RPI::AssetUtils::GetAssetIdForProductPath(DecalMaterialPath);
+
+        m_decalMaterial = AZ::Data::AssetManager::Instance().GetAsset<AZ::RPI::MaterialAsset>(
+            id, AZ::Data::AssetLoadBehavior::PreLoad);
+        m_decalMaterial.BlockUntilLoadComplete();
+    }
+
+} // namespace AtomSampleViewer

+ 258 - 0
Gem/Code/Source/LightCullingExampleComponent.h

@@ -0,0 +1,258 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+#include <AzCore/Math/Aabb.h>
+#include <AzCore/Math/Color.h>
+#include <AzCore/Math/Quaternion.h>
+#include <AzCore/Math/Random.h>
+#include <Utils/ImGuiSidebar.h>
+
+#include <Atom/Feature/CoreLights/DiskLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/PointLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/SpotLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/CapsuleLightFeatureProcessorInterface.h>
+#include <Atom/Feature/Decals/DecalFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/QuadLightFeatureProcessorInterface.h>
+
+namespace AZ
+{
+    namespace RPI
+    {
+        using AuxGeomDrawPtr = AZStd::shared_ptr<class AuxGeomDraw>;
+    }
+
+    namespace Data
+    {
+        class AssetData;
+    }
+}
+
+namespace AtomSampleViewer
+{
+    class LightCullingExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(LightCullingExampleComponent, "56B28789-4104-49B1-9C67-1DFC440DD800", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        LightCullingExampleComponent();
+        ~LightCullingExampleComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+
+    private:
+
+        enum class LightType
+        {
+            Point,
+            Spot,
+            Disk,
+            Capsule,
+            Quad,
+            Decal,
+            Count
+        };
+
+        struct LightSettings
+        {
+            bool m_enableDebugDraws = false;
+            float m_intensity = 40.0f;
+            float m_attenuationRadius = 3.0f;
+            bool m_enableAutomaticFalloff = true;
+            int m_numActive = 0;
+        };
+
+        using PointLightHandle = AZ::Render::PointLightFeatureProcessorInterface::LightHandle;
+        using SpotLightHandle = AZ::Render::SpotLightFeatureProcessorInterface::LightHandle;
+        using DiskLightHandle = AZ::Render::DiskLightFeatureProcessorInterface::LightHandle;
+        using CapsuleLightHandle = AZ::Render::CapsuleLightFeatureProcessorInterface::LightHandle;
+        using QuadLightHandle = AZ::Render::QuadLightFeatureProcessorInterface::LightHandle;
+
+        template<typename LightHandle>
+        struct Light
+        {
+            AZ::Vector3 m_position;
+            AZ::Vector3 m_direction;
+            AZ::Color m_color;
+            LightHandle m_lightHandle;
+        };
+
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        // CommonSampleComponentBase overrides...
+        void OnAllAssetsReadyActivate() override;
+
+        void DrawDebuggingHelpers();
+        void DrawPointLightDebugSpheres(AZ::RPI::AuxGeomDrawPtr auxGeom);
+        void DrawSpotLightDebugCones(AZ::RPI::AuxGeomDrawPtr auxGeom);
+        void DrawDiskLightDebugObjects(AZ::RPI::AuxGeomDrawPtr auxGeom);
+        void DrawCapsuleLightDebugObjects(AZ::RPI::AuxGeomDrawPtr auxGeom);
+        void DrawDecalDebugBoxes(AZ::RPI::AuxGeomDrawPtr auxGeom);
+
+        void SaveCameraConfiguration();
+        void RestoreCameraConfiguration();
+        void SetupScene();
+
+        void CreateOpaqueModels();
+        void DestroyOpaqueModels();
+
+        void CreateTransparentModels();
+        void DestroyTransparentModels();
+
+        void SetupCamera();
+
+        void CreatePointLights();
+        void CreatePointLight(int index);
+
+        void CreateSpotLights();
+        void CreateSpotLight(int index);
+
+        void CreateDiskLights();
+        void CreateDiskLight(int index);
+
+        void CreateCapsuleLights();
+        void CreateCapsuleLight(int index);
+
+        void CreateQuadLights();
+        void CreateQuadLight(int index);
+
+        template<typename FP, typename LA>
+        void DestroyLights(FP* fp, LA& lightArray);
+
+        void CreateDecals();
+        void CreateDecal(int index);
+        void DestroyDecals();
+
+        AZ::Color GetRandomColor();
+
+        void DrawSidebar();
+        void DrawSidebarTimingSection();
+        void DrawSidebarTimingSectionCPU();
+
+        void UpdateLights();
+
+        void CreateLightsAndDecals();
+
+        void DestroyLightsAndDecals();
+
+        void DrawSidebarPointLightsSection(LightSettings* lightSettings);
+        void DrawSidebarSpotLightsSection(LightSettings* lightSettings);
+        void DrawSidebarDiskLightsSection(LightSettings* lightSettings);
+        void DrawSidebarCapsuleLightSection(LightSettings* lightSettings);
+        void DrawSidebarDecalSection(LightSettings* lightSettings);
+        void DrawSidebarQuadLightsSections(LightSettings* lightSettings);
+
+        void DrawSidebarHeatmapOpacity();
+
+        using DecalHandle = AZ::Render::DecalFeatureProcessorInterface::DecalHandle;
+
+        struct Decal
+        {
+            AZ::Vector3 m_position;
+            AZ::Quaternion m_quaternion;
+            float m_opacity;
+            float m_angleAttenuation;
+            DecalHandle m_decalHandle;
+        };
+
+        void CalculateSmoothedFPS(float deltaTime);
+        AZ::Vector3 GetRandomPositionInsideWorldModel();
+        AZ::Vector3 GetRandomDirection();
+        float GetRandomNumber(float low, float high);
+        void InitLightArrays();
+        void OnModelReady(AZ::Data::Instance<AZ::RPI::Model> model);
+        void GetFeatureProcessors();
+
+        static float AutoCalculateAttenuationRadius(const AZ::Color& color, float intensity);
+        void MoveCameraToStartPosition();
+        void UpdateHeatmapOpacity();
+        void DisableHeatmap();
+        void DrawQuadLightDebugObjects(AZ::RPI::AuxGeomDrawPtr auxGeom);
+        void LoadDecalMaterial();
+        AZStd::array<LightSettings, (size_t)LightType::Count> m_settings;
+
+        AZStd::vector<Light<PointLightHandle>> m_pointLights;
+        AZStd::vector<Light<SpotLightHandle>> m_spotLights;
+        AZStd::vector<Light<DiskLightHandle>> m_diskLights;
+        AZStd::vector<Light<CapsuleLightHandle>> m_capsuleLights;
+        AZStd::vector<Light<QuadLightHandle>> m_quadLights;
+        AZStd::vector<Decal> m_decals;
+        AZStd::vector<AZ::Render::MeshFeatureProcessorInterface::MeshHandle> m_transparentMeshHandles;
+
+        float m_originalFarClipDistance = 0.f;
+
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+        AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler m_meshChangedHandler
+        {
+            [&](AZ::Data::Instance<AZ::RPI::Model> model) { OnModelReady(model); }
+        };
+        bool m_worldModelAssetLoaded = false;
+
+        AZ::Aabb m_worldModelAABB;
+        AZ::SimpleLcgRandom m_random;
+
+        ImGuiSidebar m_imguiSidebar;
+
+        float m_smoothedFPS = 0.0f;
+        float m_bulbRadius = 3.0f;
+
+        float m_spotInnerConeDegrees = 22.0f;
+        float m_spotOuterConeDegrees = 90.0f;
+
+        float m_capsuleRadius = 0.1f;
+        float m_capsuleLength = 3.0f;
+
+        float m_diskRadius = 3.0f;
+        bool m_isDiskDoubleSided = false;
+        bool m_isQuadLightDoubleSided = false;
+        bool m_quadLightsUseFastApproximation = false;
+
+        AZStd::array<float, 3> m_decalSize = {
+            { 5, 5, 5 }
+        };
+        float m_decalAngleAttenuation = 0.0f;
+        float m_decalOpacity = 1.0f;
+
+        bool m_refreshLights = false;
+        float m_heatmapOpacity = 0.0f;
+        AZStd::array<float, 2> m_quadLightSize = { 4, 2 };
+        AZ::Data::Asset<AZ::Data::AssetData> m_decalMaterial;
+
+        AZ::Render::PointLightFeatureProcessorInterface* m_pointLightFeatureProcessor = nullptr;
+        AZ::Render::SpotLightFeatureProcessorInterface* m_spotLightFeatureProcessor = nullptr;
+        AZ::Render::DiskLightFeatureProcessorInterface* m_diskLightFeatureProcessor = nullptr;
+        AZ::Render::CapsuleLightFeatureProcessorInterface* m_capsuleLightFeatureProcessor = nullptr;
+        AZ::Render::QuadLightFeatureProcessorInterface* m_quadLightFeatureProcessor = nullptr;
+        AZ::Render::DecalFeatureProcessorInterface* m_decalFeatureProcessor = nullptr;
+    };
+
+    template<typename FP, typename LA>
+    inline void AtomSampleViewer::LightCullingExampleComponent::DestroyLights(FP* fp, LA& lightArray)
+    {
+        for (auto& elem : lightArray)
+        {
+            fp->ReleaseLight(elem.m_lightHandle);
+        }
+    }
+} // namespace AtomSampleViewer

+ 272 - 0
Gem/Code/Source/MSAA_RPI_ExampleComponent.cpp

@@ -0,0 +1,272 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MSAA_RPI_ExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/RPI.Public/View.h>
+#include <Atom/RPI.Public/Image/StreamingImage.h>
+
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+
+#include <Utils/Utils.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <Atom/Bootstrap/DefaultWindowBus.h>
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    static const char* GetPipelineName(int numSamples)
+    {
+        switch (numSamples)
+        {
+        case 1:
+            return "No_MSAA_RPI_Pipeline";
+        case 2:
+            return "MSAA_2x_RPI_Pipeline";
+        case 4:
+            return "MSAA_4x_RPI_Pipeline";
+        case 8:
+            return "MSAA_8x_RPI_Pipeline";
+        }
+        AZ_Warning("MSAA_RPI_ExampleComponent", false, "Unsupported number of samples, defaulting to 1");
+        return "No_MSAA_RPI_Pipeline";
+    }
+
+    void MSAA_RPI_ExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class < MSAA_RPI_ExampleComponent, AZ::Component>()
+                ->Version(0)
+            ;
+        }
+    }
+
+    void MSAA_RPI_ExampleComponent::Activate()
+    {
+        m_imguiSidebar.Activate();
+        AZ::TickBus::Handler::BusConnect();
+        ExampleComponentRequestBus::Handler::BusConnect(GetEntityId());
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusConnect();
+        ActivateMSAAPipeline();
+        EnableArcBallCameraController();
+        ActivateModel();
+        ActivateIbl();
+    }
+
+    void MSAA_RPI_ExampleComponent::Deactivate()
+    {
+        DeactivateIbl();
+        DeactivateModel();
+        DisableArcBallCameraController();
+        DeactivateMSAAPipeline();
+        AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler::BusDisconnect();
+        ExampleComponentRequestBus::Handler::BusDisconnect();
+        AZ::TickBus::Handler::BusDisconnect();
+        m_imguiSidebar.Deactivate();
+    }
+
+    void MSAA_RPI_ExampleComponent::EnableArcBallCameraController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::ArcBallControllerComponent>());
+    }
+
+    void MSAA_RPI_ExampleComponent::DisableArcBallCameraController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+    }
+
+    void MSAA_RPI_ExampleComponent::OnModelReady(AZ::Data::Instance<AZ::RPI::Model> model)
+    {
+        AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset = model->GetModelAsset();
+        m_meshChangedHandler.Disconnect();
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+    }
+
+    void MSAA_RPI_ExampleComponent::ActivateMSAAPipeline()
+    {
+        CreateMSAAPipeline(m_numSamples);
+
+        AZ::RPI::ScenePtr defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        m_originalPipeline = defaultScene->GetDefaultRenderPipeline();
+        defaultScene->AddRenderPipeline(m_msaaPipeline);
+        m_msaaPipeline->SetDefaultView(m_originalPipeline->GetDefaultView());
+        defaultScene->RemoveRenderPipeline(m_originalPipeline->GetId());
+
+        // Create an ImGuiActiveContextScope to ensure the ImGui context on the new pipeline's ImGui pass is activated.
+        m_imguiScope = AZ::Render::ImGuiActiveContextScope::FromPass(AZ::RPI::PassHierarchyFilter({ m_msaaPipeline->GetId().GetCStr(), "ImGuiPass" }));
+    }
+
+    void MSAA_RPI_ExampleComponent::DeactivateMSAAPipeline()
+    {
+        m_imguiScope = {}; // restores previous ImGui context.
+
+        AZ::RPI::ScenePtr defaultScene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        defaultScene->AddRenderPipeline(m_originalPipeline);
+        AZ::RPI::RenderPipelineDescriptor pipelineDesc;
+        defaultScene->RemoveRenderPipeline(m_msaaPipeline->GetId());
+        DestroyMSAAPipeline();
+    }
+
+    void MSAA_RPI_ExampleComponent::CreateMSAAPipeline(int numSamples)
+    {
+        AZ::RPI::RenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.m_mainViewTagName = "MainCamera";
+        pipelineDesc.m_name = "MSAA";
+        pipelineDesc.m_rootPassTemplate = GetPipelineName(numSamples);
+
+        m_msaaPipeline = AZ::RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext);
+    }
+
+    void MSAA_RPI_ExampleComponent::ResetCamera()
+    {
+        const float pitch = -AZ::Constants::QuarterPi - 0.025f;
+        const float heading = AZ::Constants::QuarterPi - 0.05f;
+        const float distance = 3.0f;
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetCenter, AZ::Vector3(0.f));
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetDistance, distance);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetMaxDistance, 50.0f);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetPitch, pitch);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetHeading, heading);
+    }
+
+    void MSAA_RPI_ExampleComponent::DefaultWindowCreated()
+    {
+        AZ::Render::Bootstrap::DefaultWindowBus::BroadcastResult(m_windowContext, &AZ::Render::Bootstrap::DefaultWindowBus::Events::GetDefaultWindowContext);
+    }
+
+    void MSAA_RPI_ExampleComponent::DestroyMSAAPipeline()
+    {
+        m_msaaPipeline = nullptr;
+    }
+
+    AZ::Data::Asset<AZ::RPI::MaterialAsset> MSAA_RPI_ExampleComponent::GetMaterialAsset()
+    {
+        AZ::RPI::AssetUtils::TraceLevel traceLevel = AZ::RPI::AssetUtils::TraceLevel::Assert;
+        return AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::MaterialAsset>(DefaultPbrMaterialPath, traceLevel);
+    }
+
+    AZ::Data::Asset<AZ::RPI::ModelAsset> MSAA_RPI_ExampleComponent::GetModelAsset()
+    {
+        AZ::RPI::AssetUtils::TraceLevel traceLevel = AZ::RPI::AssetUtils::TraceLevel::Assert;
+        switch (m_modelType)
+        {
+        case 0:
+            return AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>("objects/cylinder.azmodel", traceLevel);
+        case 1:
+            return AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>("objects/cube.azmodel", traceLevel);
+        case 2:
+            return AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>("objects/shaderball_simple.azmodel", traceLevel);
+        }
+
+        AZ_Warning("MSAA_RPI_ExampleComponent", false, "Unsupported model type");
+        return AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>("objects/cylinder.azmodel", traceLevel);
+    }
+
+
+    void MSAA_RPI_ExampleComponent::ActivateModel()
+    {
+        m_meshHandle = GetMeshFeatureProcessor()->AcquireMesh(GetModelAsset(), AZ::RPI::Material::FindOrCreate(GetMaterialAsset()));
+        GetMeshFeatureProcessor()->SetTransform(m_meshHandle, AZ::Transform::CreateIdentity());
+
+        AZ::Data::Instance<AZ::RPI::Model> model = GetMeshFeatureProcessor()->GetModel(m_meshHandle);
+        if (model)
+        {
+            OnModelReady(model);
+        }
+        else
+        {
+            ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScript);
+            GetMeshFeatureProcessor()->ConnectModelChangeEventHandler(m_meshHandle, m_meshChangedHandler);
+        }
+    }
+
+    void MSAA_RPI_ExampleComponent::DeactivateModel()
+    {
+        GetMeshFeatureProcessor()->ReleaseMesh(m_meshHandle);
+    }
+
+    void MSAA_RPI_ExampleComponent::ActivateIbl()
+    {
+        m_defaultIbl.Init(AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get());
+
+        // reduce the exposure so the model isn't overly bright
+        m_defaultIbl.SetExposure(-0.5f);
+    }
+
+    void MSAA_RPI_ExampleComponent::DeactivateIbl()
+    {
+        m_defaultIbl.Reset();
+    }
+
+    void MSAA_RPI_ExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        DrawSidebar();
+    }
+
+    void MSAA_RPI_ExampleComponent::DrawSidebar()
+    {
+        if (!m_imguiSidebar.Begin())
+        {
+            return;
+        }
+
+        bool refresh = false;
+        refresh = DrawSidebarModeChooser(refresh);
+        refresh = DrawSideBarModelChooser(refresh);
+        m_imguiSidebar.End();
+
+        if (refresh)
+        {
+            // Note that the model's have some multisample information embedded into their pipeline state, so delete and recreate the model
+            DeactivateModel();
+            DeactivateMSAAPipeline();
+            ActivateMSAAPipeline();
+            ActivateModel();
+        }
+    }
+
+    bool MSAA_RPI_ExampleComponent::DrawSidebarModeChooser(bool refresh)
+    {
+        ScriptableImGui::ScopedNameContext context{ "Mode" };
+
+        ImGui::Text("Num Samples");
+        refresh |= ScriptableImGui::RadioButton("MSAA None", &m_numSamples, 1);
+        refresh |= ScriptableImGui::RadioButton("MSAA 2x", &m_numSamples, 2);
+        refresh |= ScriptableImGui::RadioButton("MSAA 4x", &m_numSamples, 4);
+        refresh |= ScriptableImGui::RadioButton("MSAA 8x", &m_numSamples, 8);
+        return refresh;
+    }
+
+    bool MSAA_RPI_ExampleComponent::DrawSideBarModelChooser(bool refresh)
+    {
+        ScriptableImGui::ScopedNameContext context{ "Model" };
+
+        ImGui::NewLine();
+        ImGui::Text("Model");
+        refresh |= ScriptableImGui::RadioButton("Cylinder", &m_modelType, 0);
+        refresh |= ScriptableImGui::RadioButton("Cube", &m_modelType, 1);
+        refresh |= ScriptableImGui::RadioButton("ShaderBall", &m_modelType, 2);
+        return refresh;
+    }
+}

+ 99 - 0
Gem/Code/Source/MSAA_RPI_ExampleComponent.h

@@ -0,0 +1,99 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+#include <Atom/Feature/ImGui/ImGuiUtils.h>
+#include <AzFramework/Input/Events/InputChannelEventListener.h>
+#include <Atom/Bootstrap/DefaultWindowBus.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/Utils.h>
+#include <ExampleComponentBus.h>
+
+namespace AtomSampleViewer
+{
+    //!
+    //! This component creates a simple scene that tests the MSAA pipeline. It can test both MSAA enabled and disabled with the same scene
+    //!
+    class MSAA_RPI_ExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::Render::Bootstrap::DefaultWindowNotificationBus::Handler
+        , public AZ::TickBus::Handler
+        , public ExampleComponentRequestBus::Handler
+    {
+    public:
+
+        AZ_COMPONENT(MSAA_RPI_ExampleComponent, "{2BDCA64E-7E5F-49EE-ACF5-65C79C40840D}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        void Activate() override;
+
+
+        void Deactivate() override;
+
+    private:
+
+        void ActivateMSAAPipeline();
+        void DeactivateMSAAPipeline();
+
+        void ActivateIbl();
+        void DeactivateIbl();
+
+        void CreateMSAAPipeline(int numSamples);
+        void DestroyMSAAPipeline();
+
+        void ActivateModel();
+        void DeactivateModel();
+        
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        // ExampleComponentRequestBus::Handler
+        void ResetCamera() override;
+
+        void DefaultWindowCreated() override;
+
+        void EnableArcBallCameraController();
+        void DisableArcBallCameraController();
+
+        void OnModelReady(AZ::Data::Instance<AZ::RPI::Model> model);
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> GetMaterialAsset();
+        AZ::Data::Asset<AZ::RPI::ModelAsset> GetModelAsset();
+        void DrawSidebar();
+
+        bool DrawSidebarModeChooser(bool refresh);
+        bool DrawSideBarModelChooser(bool refresh);
+
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+        AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler m_meshChangedHandler
+        {
+            [&](AZ::Data::Instance<AZ::RPI::Model> model)
+            {
+                OnModelReady(model);
+            }
+        };
+        
+        AZ::RPI::RenderPipelinePtr m_msaaPipeline;
+        AZ::RPI::RenderPipelinePtr m_originalPipeline;
+        AZStd::shared_ptr<AZ::RPI::WindowContext> m_windowContext;
+        Utils::DefaultIBL m_defaultIbl;
+        ImGuiSidebar m_imguiSidebar;
+        int m_numSamples = 1;
+        int m_modelType = 0;
+
+        AZ::Render::ImGuiActiveContextScope m_imguiScope;
+    };
+} // namespace AtomSampleViewer

+ 604 - 0
Gem/Code/Source/MaterialHotReloadTestComponent.cpp

@@ -0,0 +1,604 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MaterialHotReloadTestComponent.h>
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/Feature/Material/MaterialAssignment.h>
+
+#include <AzCore/IO/Path/Path.h>
+#include <AzFramework/IO/LocalFileIO.h>
+#include <AzFramework/Asset/AssetSystemBus.h>
+#include <AzFramework/Asset/AssetProcessorMessages.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <AzCore/Utils/Utils.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+    using namespace RPI;
+
+    namespace
+    {
+        namespace Sources
+        {
+            static constexpr const char MaterialFileName[] = "HotReloadTest.material";
+            static constexpr const char MaterialTypeFileName[] = "HotReloadTest.materialtype";
+            static constexpr const char ShaderFileName[] = "HotReloadTest.shader";
+            static constexpr const char AzslFileName[] = "HotReloadTest.azsl";
+            static constexpr const char ShaderVariantListFileName[] = "HotReloadTest.shadervariantlist";
+        }
+
+        namespace Products
+        {
+
+            static constexpr const char ModelFilePath[] = "objects/plane.azmodel";
+            static constexpr const char MaterialFilePath[] = "materials/hotreloadtest/temp/hotreloadtest.azmaterial";
+            static constexpr const char MaterialTypeFilePath[] = "materials/hotreloadtest/temp/hotreloadtest.azmaterialtype";
+            static constexpr const char ShaderFilePath[] = "materials/hotreloadtest/temp/hotreloadtest.azshader";
+
+            static constexpr const char ShaderVariantTreeFileName[] = "hotreloadtest.azshadervarianttree";
+
+            static AZStd::string GetShaderVariantFileName(RPI::ShaderVariantStableId stableId)
+            {
+                RPISystemInterface* rpiSystem = RPISystemInterface::Get();
+                Name apiName = rpiSystem->GetRenderApiName();
+                AZStd::string filePath = AZStd::string::format("hotreloadtest_%s_%u.azshadervariant", apiName.GetCStr(), stableId.GetIndex());
+                return filePath;
+            }
+
+            // These materials are used to communicate which shader variant is being used, for screenshot comparison tests.
+            static constexpr const char RootVariantIndicatorMaterialFilePath[] = "materials/hotreloadtest/testdata/variantselection_root.azmaterial";
+            static constexpr const char FullyBakedVariantIndicatorMaterialFilePath[] = "materials/hotreloadtest/testdata/variantselection_fullybaked.azmaterial";
+        }
+
+        namespace TestData
+        {
+            namespace MaterialFileNames
+            {
+                static constexpr const char Default[] = "Material_UseDefaults.txt";
+                static constexpr const char ChangePrimaryToRed[] = "Material_ChangePrimaryToRed.txt";
+                static constexpr const char ChangePrimaryToBlue[] = "Material_ChangePrimaryToBlue.txt";
+            }
+
+            namespace MaterialTypeFileNames
+            {
+                static constexpr const char StraightLines[] = "MaterialType_StraightLines.txt";
+                static constexpr const char WavyLines[] = "MaterialType_WavyLines.txt";
+            }
+
+            namespace ShaderFileNames
+            {
+                static constexpr const char BlendingOff[] = "Shader_BlendingOff.txt";
+                static constexpr const char BlendingOn[] = "Shader_BlendingOn.txt";
+            }
+
+            namespace AzslFileNames
+            {
+                static constexpr const char HorizontalPattern[] = "AZSL_HorizontalPattern.txt";
+                static constexpr const char VerticalPattern[] = "AZSL_VerticalPattern.txt";
+            }
+
+            namespace ShaderVariantListFileNames
+            {
+                static constexpr const char All[] = "ShaderVariantList_All.txt";
+                static constexpr const char OnlyStraightLines[] = "ShaderVariantList_OnlyStraightLines.txt";
+                static constexpr const char OnlyWavyLines[] = "ShaderVariantList_OnlyWavyLines.txt";
+            }
+        }
+    }
+
+    void MaterialHotReloadTestComponent::Reflect(ReflectContext* context)
+    {
+        if (SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context))
+        {
+            serializeContext->Class<MaterialHotReloadTestComponent, CommonSampleComponentBase>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    MaterialHotReloadTestComponent::MaterialHotReloadTestComponent()
+        : m_imguiSidebar{"@user@/MaterialHotReloadTestComponent/sidebar.xml"}
+    {
+    }
+    
+    void MaterialHotReloadTestComponent::InitTestDataFolders()
+    {
+        auto io = AZ::IO::LocalFileIO::GetInstance();
+
+        AZStd::string appRoot;
+        AzFramework::ApplicationRequests::Bus::BroadcastResult(appRoot, &AzFramework::ApplicationRequests::GetAppRoot);
+        AZStd::string mainTestFolder;
+        AzFramework::StringFunc::Path::Join(appRoot.c_str(), "AtomSampleViewer/Materials/HotReloadTest/", mainTestFolder);
+        AzFramework::StringFunc::Path::Join(mainTestFolder.c_str(), "TestData/", m_testDataFolder);
+        if (!io->Exists(m_testDataFolder.c_str()))
+        {
+            AZ_Error("MaterialHotReloadTestComponent", false, "Could not find source folder '%s'. This sample can only be used on dev platforms.", m_testDataFolder.c_str());
+            m_testDataFolder.clear();
+            return;
+        }
+
+        AzFramework::StringFunc::Path::Join(mainTestFolder.c_str(), "Temp/", m_tempSourceFolder);
+        if (!io->CreatePath(m_tempSourceFolder.c_str()))
+        {
+            AZ_Error("MaterialHotReloadTestComponent", false, "Could not create temp folder '%s'.", m_tempSourceFolder.c_str());
+        }
+    }
+
+    void MaterialHotReloadTestComponent::Activate()
+    {
+        m_initStatus = InitStatus::None;
+        
+        TickBus::Handler::BusConnect();
+        m_imguiSidebar.Activate();
+
+        InitTestDataFolders();
+
+        // Delete any existing temp files and wait for the assets to disappear, to ensure we have a clean slate.
+        // (If we were to just replace existing temp source files with the default ones without fully
+        //  removing them first, it would be tricky to figure out whether the assets loaded assets are the new
+        //  ones or stale ones left over from a prior instance of this sample).
+        DeleteTestFile(Sources::MaterialFileName);
+        DeleteTestFile(Sources::MaterialTypeFileName);
+        DeleteTestFile(Sources::ShaderFileName);
+        DeleteTestFile(Sources::AzslFileName);
+        DeleteTestFile(Sources::ShaderVariantListFileName);
+
+        m_initStatus = InitStatus::ClearingTestAssets;
+
+        // Wait until the test material is fully initialized. Use a long timeout because it can take a while for the shaders to compile.
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScriptWithTimeout, LongTimeout);
+
+        Data::AssetId modelAssetId;
+        Data::AssetCatalogRequestBus::BroadcastResult(modelAssetId, &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, Products::ModelFilePath, nullptr, false);
+        AZ_Assert(modelAssetId.IsValid(), "Failed to get model asset id: %s", Products::ModelFilePath);
+        m_modelAsset.Create(modelAssetId);
+
+        const Transform cameraTransform = Transform::CreateFromQuaternionAndTranslation(
+            Quaternion::CreateFromAxisAngle(Vector3::CreateAxisZ(), AZ::Constants::Pi),
+            Vector3{0.0f, 2.0f, 0.0f}
+        );
+        AZ::TransformBus::Event(GetCameraEntityId(), &AZ::TransformBus::Events::SetWorldTM, cameraTransform);
+
+        m_meshFeatureProcessor = Scene::GetFeatureProcessorForEntityContextId<Render::MeshFeatureProcessorInterface>(GetEntityContextId());
+
+        // The shader variant indicator banner will appear right below the main test mesh
+        m_shaderVariantIndicatorMeshTransform = Transform::CreateIdentity();
+        m_shaderVariantIndicatorMeshTransform.SetTranslation(Vector3{0.0f, 0.0f, -0.6f});
+        m_shaderVariantIndicatorMeshTransform.SetScale(Vector3{1.0f, 0.125f, 1.0f});
+        m_shaderVariantIndicatorMeshTransform.SetRotation(Quaternion::CreateFromAxisAngle(Vector3::CreateAxisX(), -AZ::Constants::HalfPi));
+
+        // Load materials that will be used to indicate which shader variant is active...
+        Data::Asset<MaterialAsset> indicatorMaterialAsset;
+        indicatorMaterialAsset = AssetUtils::LoadAssetByProductPath<MaterialAsset>(Products::RootVariantIndicatorMaterialFilePath, AssetUtils::TraceLevel::Assert);
+        m_shaderVariantIndicatorMaterial_root = Material::FindOrCreate(indicatorMaterialAsset);
+        indicatorMaterialAsset = AssetUtils::LoadAssetByProductPath<MaterialAsset>(Products::FullyBakedVariantIndicatorMaterialFilePath, AssetUtils::TraceLevel::Assert);
+        m_shaderVariantIndicatorMaterial_fullyBaked = Material::FindOrCreate(indicatorMaterialAsset);
+    }
+
+    void MaterialHotReloadTestComponent::Deactivate()
+    {
+        m_meshFeatureProcessor->ReleaseMesh(m_meshHandle);
+        m_meshFeatureProcessor->ReleaseMesh(m_shaderVariantIndicatorMeshHandle);
+        m_shaderVariantIndicatorMaterial_root.reset();
+        m_shaderVariantIndicatorMaterial_fullyBaked.reset();
+        m_shaderVariantIndicatorMaterial_current.reset();
+
+        Data::AssetBus::Handler::BusDisconnect();
+        TickBus::Handler::BusDisconnect();
+        m_imguiSidebar.Deactivate();
+        
+        m_initStatus = InitStatus::None;
+
+    }
+
+    void MaterialHotReloadTestComponent::DeleteTestFile(const char* tempSourceFile)
+    {
+        AZ::IO::Path deletePath = AZ::IO::Path(m_tempSourceFolder).Append(tempSourceFile);
+
+        if (AZ::IO::LocalFileIO::GetInstance()->Exists(deletePath.c_str()))
+        {
+            m_fileIoErrorHandler.BusConnect();
+
+            if (!AZ::IO::LocalFileIO::GetInstance()->Remove(deletePath.c_str()))
+            {
+                m_fileIoErrorHandler.ReportLatestIOError(AZStd::string::format("Failed to delete '%s'.", deletePath.c_str()));
+            }
+
+            m_fileIoErrorHandler.BusDisconnect();
+        }
+    }
+
+    void MaterialHotReloadTestComponent::CopyTestFile(const AZStd::string& testDataFile, const AZStd::string& tempSourceFile)
+    {
+        // Instead of copying the file using AZ::IO::LocalFileIO, we load the file and write out a new file over top
+        // the destination. This is necessary to make the AP reliably detect the changes (if we just copy the file,
+        // sometimes it recognizes the OS level copy as an updated file and sometimes not).
+
+        AZ::IO::Path copyFrom = AZ::IO::Path(m_testDataFolder).Append(testDataFile);
+        AZ::IO::Path copyTo = AZ::IO::Path(m_tempSourceFolder).Append(tempSourceFile);
+
+        m_fileIoErrorHandler.BusConnect();
+
+        auto readResult = AZ::Utils::ReadFile(copyFrom.c_str());
+        if (!readResult.IsSuccess())
+        {
+            m_fileIoErrorHandler.ReportLatestIOError(readResult.GetError());
+            return;
+        }
+
+        auto writeResult = AZ::Utils::WriteFile(readResult.GetValue(), copyTo.c_str());
+        if (!writeResult.IsSuccess())
+        {
+            m_fileIoErrorHandler.ReportLatestIOError(writeResult.GetError());
+            return;
+        }
+
+        m_fileIoErrorHandler.BusDisconnect();
+    }
+
+    const char* ToString(AzFramework::AssetSystem::AssetStatus status)
+    {
+        switch (status)
+        {
+            case AzFramework::AssetSystem::AssetStatus_Missing: return "Missing";
+            case AzFramework::AssetSystem::AssetStatus_Queued: return "Queued...";
+            case AzFramework::AssetSystem::AssetStatus_Compiling: return "Compiling...";
+            case AzFramework::AssetSystem::AssetStatus_Compiled: return "Compiled";
+            case AzFramework::AssetSystem::AssetStatus_Failed: return "Failed";
+            default: return "Unknown";
+        }
+    }
+
+    AzFramework::AssetSystem::AssetStatus MaterialHotReloadTestComponent::GetTestAssetStatus(const char* tempSourceFile) const
+    {
+        AZStd::string filePath = AZStd::string("materials/hotreloadtest/temp/") + tempSourceFile;
+
+        AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus::AssetStatus_Unknown;
+        AzFramework::AssetSystemRequestBus::BroadcastResult(status, &AzFramework::AssetSystem::AssetSystemRequests::GetAssetStatusSearchType,
+            filePath.c_str(), AzFramework::AssetSystem::RequestAssetStatus::SearchType::Exact);
+
+        return status;
+    }
+
+    void MaterialHotReloadTestComponent::DrawAssetStatus(const char* tempSourceFile, bool includeFileName)
+    {
+        AzFramework::AssetSystem::AssetStatus status = GetTestAssetStatus(tempSourceFile);
+
+        if (includeFileName)
+        {
+            ImGui::Text("%s", tempSourceFile);
+            ImGui::Indent();
+        }
+
+        ImGui::Text("Status:");
+        ImGui::SameLine();
+        ImGui::Text(ToString(status));
+
+        if (includeFileName)
+        {
+            ImGui::Unindent();
+        }
+    }
+
+    Data::AssetId MaterialHotReloadTestComponent::GetAssetId(const char* productFilePath)
+    {
+        Data::AssetId assetId;
+        Data::AssetCatalogRequestBus::BroadcastResult(assetId, &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, productFilePath, nullptr, false);
+        return assetId;
+    }
+
+    void MaterialHotReloadTestComponent::OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset)
+    {
+        if (m_initStatus == InitStatus::WaitingForDefaultMaterialToLoad && asset.GetId() == m_materialAsset.GetId())
+        {
+            m_materialAsset = asset;
+            Data::AssetBus::Handler::BusDisconnect(asset.GetId());
+            m_initStatus = InitStatus::Ready;
+
+            // Now that we finally have the material asset, we can add the model to the scene...
+
+            m_material = Material::Create(m_materialAsset);
+
+            Transform meshTransform = Transform::CreateFromQuaternion(Quaternion::CreateFromAxisAngle(Vector3::CreateAxisX(), -AZ::Constants::HalfPi));
+            m_meshHandle = m_meshFeatureProcessor->AcquireMesh(m_modelAsset, m_material);
+            m_meshFeatureProcessor->SetTransform(m_meshHandle, meshTransform);
+
+            Data::Instance<RPI::Model> model = GetMeshFeatureProcessor()->GetModel(m_meshHandle);
+            if (model)
+            {
+                // Both the model and material are ready so scripts can continue
+                ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+            }
+            else
+            {
+                // The material is ready but the model isn't ready yet; wait until it's ready before allowing scripts to continue
+                m_meshChangedHandler = AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler
+                    {
+                        [](AZ::Data::Instance<AZ::RPI::Model> model) { ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript); }
+                    };
+                GetMeshFeatureProcessor()->ConnectModelChangeEventHandler(m_meshHandle, m_meshChangedHandler);
+            }
+        }
+    }
+
+    MaterialHotReloadTestComponent::ShaderVariantStatus MaterialHotReloadTestComponent::GetShaderVariantStatus() const
+    {
+        ShaderVariantStatus shaderVariantStatus = ShaderVariantStatus::None;
+
+        if (m_material)
+        {
+            const ShaderVariantId variantId = m_material->GetShaderCollection()[0].GetShaderVariantId();
+            auto searchResult = m_material->GetShaderCollection()[0].GetShaderAsset()->FindVariantStableId(variantId);
+            if (searchResult.IsFullyBaked())
+            {
+                shaderVariantStatus = ShaderVariantStatus::FullyBaked;
+            }
+            else if (searchResult.IsRoot())
+            {
+                shaderVariantStatus = ShaderVariantStatus::Root;
+            }
+            else
+            {
+                shaderVariantStatus = ShaderVariantStatus::PartiallyBaked;
+            }
+        }
+
+        return shaderVariantStatus;
+    }
+
+    void MaterialHotReloadTestComponent::OnTick([[maybe_unused]] float deltaTime, ScriptTimePoint /*scriptTime*/)
+    {
+        if (m_initStatus == InitStatus::ClearingTestAssets)
+        {
+            Data::AssetId materialAssetId = GetAssetId(Products::MaterialFilePath);
+            Data::AssetId materialTypeAssetId = GetAssetId(Products::MaterialTypeFilePath);
+            Data::AssetId shaderAssetId = GetAssetId(Products::ShaderFilePath);
+
+            if (!materialAssetId.IsValid() &&
+                !materialTypeAssetId.IsValid() &&
+                !shaderAssetId.IsValid())
+            {
+                // [GFX TODO] [ATOM-5899] Once this ticket is addressed, This block can call all required CopyTestFile() at once,
+                //            and the states InitStatus::CopyingDefault***TestFile won't be needed.
+                // Any stale assets have been cleared, now we can create the new ones.
+                // Copy a default set files into the temp folder.
+                CopyTestFile(TestData::AzslFileNames::HorizontalPattern, Sources::AzslFileName);
+                m_initStatus = InitStatus::CopyingDefaultAzslTestFile;
+            }
+        }
+
+        if (m_initStatus == InitStatus::CopyingDefaultAzslTestFile)
+        {
+            AzFramework::AssetSystem::AssetStatus status = GetTestAssetStatus(Sources::AzslFileName);
+            if (status == AzFramework::AssetSystem::AssetStatus::AssetStatus_Compiled)
+            {
+                CopyTestFile(TestData::ShaderFileNames::BlendingOff, Sources::ShaderFileName);
+                m_initStatus = InitStatus::CopyingDefaultShaderTestFile;
+            }
+        }
+        else if (m_initStatus == InitStatus::CopyingDefaultShaderTestFile)
+        {
+            AzFramework::AssetSystem::AssetStatus status = GetTestAssetStatus(Sources::ShaderFileName);
+            if (status == AzFramework::AssetSystem::AssetStatus::AssetStatus_Compiled)
+            {
+                CopyTestFile(TestData::MaterialTypeFileNames::StraightLines, Sources::MaterialTypeFileName);
+                m_initStatus = InitStatus::CopyingDefaultMaterialTypeTestFile;
+            }
+        }
+        else if (m_initStatus == InitStatus::CopyingDefaultMaterialTypeTestFile)
+        {
+            AzFramework::AssetSystem::AssetStatus status = GetTestAssetStatus(Sources::MaterialTypeFileName);
+            if (status == AzFramework::AssetSystem::AssetStatus::AssetStatus_Compiled)
+            {
+                CopyTestFile(TestData::MaterialFileNames::Default, Sources::MaterialFileName);
+                m_initStatus = InitStatus::WaitingForDefaultMaterialToRegister;
+            }
+        }
+
+        if (m_initStatus == InitStatus::WaitingForDefaultMaterialToRegister)
+        {
+            Data::AssetId materialAssetId = GetAssetId(Products::MaterialFilePath);
+            if (materialAssetId.IsValid())
+            {
+                m_initStatus = InitStatus::WaitingForDefaultMaterialToLoad;
+                Data::AssetBus::Handler::BusConnect(materialAssetId);
+                m_materialAsset.Create(materialAssetId, true);
+            }
+        }
+
+        auto shaderVariantStatus = GetShaderVariantStatus();
+        if (shaderVariantStatus == ShaderVariantStatus::None)
+        {
+            m_meshFeatureProcessor->ReleaseMesh(m_shaderVariantIndicatorMeshHandle);
+            m_shaderVariantIndicatorMaterial_current.reset();
+        }
+        else
+        {
+            bool updateIndicatorMesh = false;
+            if (shaderVariantStatus == ShaderVariantStatus::Root)
+            {
+                if (m_shaderVariantIndicatorMaterial_current != m_shaderVariantIndicatorMaterial_root)
+                {
+                    m_shaderVariantIndicatorMaterial_current = m_shaderVariantIndicatorMaterial_root;
+                    updateIndicatorMesh = true;
+                }
+            }
+            else if(shaderVariantStatus == ShaderVariantStatus::FullyBaked)
+            {
+                if (m_shaderVariantIndicatorMaterial_current != m_shaderVariantIndicatorMaterial_fullyBaked)
+                {
+                    m_shaderVariantIndicatorMaterial_current = m_shaderVariantIndicatorMaterial_fullyBaked;
+                    updateIndicatorMesh = true;
+                }
+            }
+            else
+            {
+                AZ_Assert(false, "Unsupported ShaderVariantStatus");
+            }
+
+            if (updateIndicatorMesh)
+            {
+                m_meshFeatureProcessor->ReleaseMesh(m_shaderVariantIndicatorMeshHandle);
+                m_shaderVariantIndicatorMeshHandle = m_meshFeatureProcessor->AcquireMesh(m_modelAsset, m_shaderVariantIndicatorMaterial_current);
+                m_meshFeatureProcessor->SetTransform(m_shaderVariantIndicatorMeshHandle, m_shaderVariantIndicatorMeshTransform);
+            }
+        }
+
+        if (m_imguiSidebar.Begin())
+        {
+            if (m_initStatus != InitStatus::Ready)
+            {
+                ImGui::Text("Waiting for assets...");
+                ImGui::Separator();
+            }
+
+            ImGui::Text(Sources::MaterialFileName);
+            ImGui::Indent();
+            {
+                DrawAssetStatus(Sources::MaterialFileName);
+
+                if (m_initStatus == InitStatus::Ready)
+                {
+                    if (ScriptableImGui::Button("Default Colors"))
+                    {
+                        CopyTestFile(TestData::MaterialFileNames::Default, Sources::MaterialFileName);
+                    }
+
+                    if (ScriptableImGui::Button("ColorA = Red"))
+                    {
+                        CopyTestFile(TestData::MaterialFileNames::ChangePrimaryToRed, Sources::MaterialFileName);
+                    }
+
+                    if (ScriptableImGui::Button("ColorA = Blue"))
+                    {
+                        CopyTestFile(TestData::MaterialFileNames::ChangePrimaryToBlue, Sources::MaterialFileName);
+                    }
+                }
+            }
+            ImGui::Unindent();
+
+            ImGui::Text(Sources::MaterialTypeFileName);
+            ImGui::Indent();
+            {
+                DrawAssetStatus(Sources::MaterialTypeFileName);
+
+                if (m_initStatus == InitStatus::Ready)
+                {
+                    if (ScriptableImGui::Button("Straight Lines"))
+                    {
+                        CopyTestFile(TestData::MaterialTypeFileNames::StraightLines, Sources::MaterialTypeFileName);
+                    }
+
+                    if (ScriptableImGui::Button("Wavy Lines"))
+                    {
+                        CopyTestFile(TestData::MaterialTypeFileNames::WavyLines, Sources::MaterialTypeFileName);
+                    }
+                }
+            }
+            ImGui::Unindent();
+
+            ImGui::Text(Sources::ShaderFileName);
+            ImGui::Indent();
+            {
+                DrawAssetStatus(Sources::ShaderFileName);
+
+                if (m_initStatus == InitStatus::Ready)
+                {
+                    if (ScriptableImGui::Button("Blending Off"))
+                    {
+                        CopyTestFile(TestData::ShaderFileNames::BlendingOff, Sources::ShaderFileName);
+                    }
+
+                    if (ScriptableImGui::Button("Blending On"))
+                    {
+                        CopyTestFile(TestData::ShaderFileNames::BlendingOn, Sources::ShaderFileName);
+                    }
+                }
+            }
+            ImGui::Unindent();
+
+            ImGui::Text(Sources::AzslFileName);
+            ImGui::Indent();
+            {
+                // The AZSL file is a source dependency of the .shader file, so display the same asset status
+                DrawAssetStatus(Sources::AzslFileName);
+
+                if (m_initStatus == InitStatus::Ready)
+                {
+                    if (ScriptableImGui::Button("Horizontal Pattern"))
+                    {
+                        CopyTestFile(TestData::AzslFileNames::HorizontalPattern, Sources::AzslFileName);
+                    }
+
+                    if (ScriptableImGui::Button("Vertical Pattern"))
+                    {
+                        CopyTestFile(TestData::AzslFileNames::VerticalPattern, Sources::AzslFileName);
+                    }
+                }
+            }
+            ImGui::Unindent();
+
+            ImGui::Text(Sources::ShaderVariantListFileName);
+            ImGui::Indent();
+            {
+                // The AZSL file is a source dependency of the .shader file, so display the same asset status
+                DrawAssetStatus(Sources::ShaderVariantListFileName, false);
+                DrawAssetStatus(Products::ShaderVariantTreeFileName, true);
+                DrawAssetStatus(Products::GetShaderVariantFileName(ShaderVariantStableId{0}).c_str(), true);
+                DrawAssetStatus(Products::GetShaderVariantFileName(ShaderVariantStableId{1}).c_str(), true);
+                DrawAssetStatus(Products::GetShaderVariantFileName(ShaderVariantStableId{2}).c_str(), true);
+
+                if (m_initStatus == InitStatus::Ready)
+                {
+                    ScriptableImGui::PushNameContext("ShaderVariantList");
+
+                    if (ScriptableImGui::Button("None"))
+                    {
+                        DeleteTestFile(Sources::ShaderVariantListFileName);
+                    }
+
+                    if (ScriptableImGui::Button("All"))
+                    {
+                        CopyTestFile(TestData::ShaderVariantListFileNames::All, Sources::ShaderVariantListFileName);
+                    }
+
+                    if (ScriptableImGui::Button("Only Wavy Lines"))
+                    {
+                        CopyTestFile(TestData::ShaderVariantListFileNames::OnlyWavyLines, Sources::ShaderVariantListFileName);
+                    }
+
+                    if (ScriptableImGui::Button("Only Straight Lines"))
+                    {
+                        CopyTestFile(TestData::ShaderVariantListFileNames::OnlyStraightLines, Sources::ShaderVariantListFileName);
+                    }
+
+                    ScriptableImGui::PopNameContext();
+                }
+            }
+            ImGui::Unindent();
+
+            m_imguiSidebar.End();
+        }
+
+    }
+
+} // namespace AtomSampleViewer

+ 117 - 0
Gem/Code/Source/MaterialHotReloadTestComponent.h

@@ -0,0 +1,117 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/FileIOErrorHandler.h>
+#include <AzCore/Component/TickBus.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <AzFramework/Asset/AssetSystemTypes.h>
+
+namespace AtomSampleViewer
+{
+    //! This test renders a simple material and exposes controls that can update the source data for that material and its shaders
+    //! to demonstrate and test hot-reloading. It works by copying entire files from a test data folder into a material source folder
+    //! and waiting for the Asset Processor to build the updates files.
+    class MaterialHotReloadTestComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+        , public AZ::Data::AssetBus::Handler
+    {
+    public:
+        AZ_COMPONENT(MaterialHotReloadTestComponent, "{EA684B21-9E39-4210-A640-AFBC28B2E683}", CommonSampleComponentBase);
+        MaterialHotReloadTestComponent();
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        // AZ::Component overrides...
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+
+        AZ_DISABLE_COPY_MOVE(MaterialHotReloadTestComponent);
+
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint scriptTime) override;
+        
+        // Finds the paths m_testDataFolder and m_tempSourceFolder
+        void InitTestDataFolders();
+
+        // Deletes a file from m_tempSourceFolder
+        void DeleteTestFile(const char* tempSourceFile);
+
+        // Copies a file from m_testDataFolder to m_tempSourceFolder
+        void CopyTestFile(const AZStd::string& testDataFile, const AZStd::string& tempSourceFile);
+
+        // Returns the AssetStatus of a file in m_tempSourceFolder 
+        AzFramework::AssetSystem::AssetStatus GetTestAssetStatus(const char* tempSourceFile) const;
+
+        // Draws ImGui indicating the Asset Processor status of a file in m_tempSourceFolder 
+        void DrawAssetStatus(const char* tempSourceFile, bool includeFileName = false);
+
+        AZ::Data::AssetId GetAssetId(const char* productFilePath);
+
+        void OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
+
+        enum class ShaderVariantStatus
+        {
+            None,
+            Root,
+            PartiallyBaked,
+            FullyBaked
+        };
+
+        ShaderVariantStatus GetShaderVariantStatus() const;
+
+        static constexpr float LongTimeout = 30.0f;
+
+        // Tracks initialization that starts when the component is activated
+        enum class InitStatus
+        {
+            None,
+            ClearingTestAssets,
+            CopyingDefaultAzslTestFile,
+            CopyingDefaultShaderTestFile,
+            CopyingDefaultMaterialTypeTestFile,
+            WaitingForDefaultMaterialToRegister,
+            WaitingForDefaultMaterialToLoad,
+            Ready
+        };
+        InitStatus m_initStatus = InitStatus::None;
+
+        AZStd::string m_testDataFolder;   //< Stores several txt files with contents to be copied over various source asset files.
+        AZStd::string m_tempSourceFolder; //< Folder for temp source asset files. These are what the sample edits and reloads.
+
+        ImGuiSidebar m_imguiSidebar;
+
+        AZ::Render::MeshFeatureProcessorInterface* m_meshFeatureProcessor = nullptr;
+
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_materialAsset;
+        AZ::Data::Instance<AZ::RPI::Material> m_material;
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_modelAsset;
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+        AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler m_meshChangedHandler;
+
+        // These are used to render a secondary mesh that indicates which shader variant is being used to render the primary mesh
+        AZ::Transform m_shaderVariantIndicatorMeshTransform;
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_shaderVariantIndicatorMeshHandle;
+        AZ::Data::Instance<AZ::RPI::Material> m_shaderVariantIndicatorMaterial_root;
+        AZ::Data::Instance<AZ::RPI::Material> m_shaderVariantIndicatorMaterial_fullyBaked;
+        AZ::Data::Instance<AZ::RPI::Material>  m_shaderVariantIndicatorMaterial_current;
+
+        FileIOErrorHandler m_fileIoErrorHandler;
+    };
+} // namespace AtomSampleViewer

+ 348 - 0
Gem/Code/Source/MeshExampleComponent.cpp

@@ -0,0 +1,348 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MeshExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/RHI/Device.h>
+#include <Atom/RHI/Factory.h>
+
+#include <Atom/RPI.Public/View.h>
+
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <AzCore/Asset/AssetManagerBus.h>
+#include <AzCore/Component/Entity.h>
+#include <AzCore/IO/IOUtils.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/std/smart_ptr/make_shared.h>
+#include <AzCore/std/sort.h>
+
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
+
+#include <SampleComponentManager.h>
+#include <SampleComponentConfig.h>
+#include <EntityUtilityFunctions.h>
+
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    const char* MeshExampleComponent::CameraControllerNameTable[CameraControllerCount] =
+    {
+        "ArcBall",
+        "NoClip"
+    };
+
+    void MeshExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MeshExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    MeshExampleComponent::MeshExampleComponent()
+        : m_materialBrowser("@user@/MeshExampleComponent/material_browser.xml")
+        , m_modelBrowser("@user@/MeshExampleComponent/model_browser.xml")
+        , m_imguiSidebar("@user@/MeshExampleComponent/sidebar.xml")
+    {
+        m_changedHandler = AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler
+        {
+            [&](AZ::Data::Instance<AZ::RPI::Model> model)
+            {
+                ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+
+                // This handler will be connected to the feature processor so that when the model is updated, the camera
+                // controller will reset. This ensures the camera is a reasonable distance from the model when it resizes.
+                ResetCameraController();
+            }
+        };
+    }
+
+    void MeshExampleComponent::Activate()
+    {
+        UseArcBallCameraController();
+
+        m_materialBrowser.SetFilter([this](const AZ::Data::AssetInfo& assetInfo)
+        {
+            if (!AzFramework::StringFunc::Path::IsExtension(assetInfo.m_relativePath.c_str(), "azmaterial"))
+            {
+                return false;
+            }
+            if (m_showModelMaterials)
+            {
+                return true;
+            }
+            // Return true only if the azmaterial was generated from a ".material" file.
+            // Materials with subid == 0, are 99.99% guaranteed to be generated from a ".material" file.
+            // Without this assurance We would need to call  AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourceUUID()
+            // to figure out what's the source of this azmaterial. But, Atom can not include AzToolsFramework.
+            return assetInfo.m_assetId.m_subId == 0;
+        });
+
+        m_modelBrowser.SetFilter([](const AZ::Data::AssetInfo& assetInfo)
+        {
+            return assetInfo.m_assetType == azrtti_typeid<AZ::RPI::ModelAsset>();
+        });
+
+        m_materialBrowser.Activate();
+        m_modelBrowser.Activate();
+        m_imguiSidebar.Activate();
+
+        InitLightingPresets(true);
+
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void MeshExampleComponent::Deactivate()
+    {
+        AZ::TickBus::Handler::BusDisconnect();
+
+        m_imguiSidebar.Deactivate();
+
+        m_materialBrowser.Deactivate();
+        m_modelBrowser.Deactivate();
+
+        RemoveController();
+
+        GetMeshFeatureProcessor()->ReleaseMesh(m_meshHandle);
+
+        m_materialOverrideInstance = nullptr;
+
+        ShutdownLightingPresets();
+    }
+
+    void MeshExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
+    {
+        bool modelNeedsUpdate = false;
+
+        if (m_imguiSidebar.Begin())
+        {
+            ImGuiLightingPreset();
+
+            ImGuiAssetBrowser::WidgetSettings assetBrowserSettings;
+
+            modelNeedsUpdate |= ScriptableImGui::Checkbox("Enable Material Override", &m_enableMaterialOverride);
+           
+            if (ScriptableImGui::Checkbox("Show Model Materials", &m_showModelMaterials))
+            {
+                modelNeedsUpdate = true;
+                m_materialBrowser.SetNeedsRefresh();
+            }
+
+            assetBrowserSettings.m_labels.m_root = "Materials";
+            modelNeedsUpdate |= m_materialBrowser.Tick(assetBrowserSettings);
+
+            ImGui::Spacing();
+            ImGui::Separator();
+            ImGui::Spacing();
+
+            assetBrowserSettings.m_labels.m_root = "Models";
+            bool modelChanged = m_modelBrowser.Tick(assetBrowserSettings);
+            modelNeedsUpdate |= modelChanged;
+
+            if (modelChanged)
+            {
+                // Reset LOD override when the model changes.
+                m_lodOverride = AZ::RPI::Cullable::NoLodOverride;
+            }
+
+            AZ::Data::Instance<AZ::RPI::Model> model = GetMeshFeatureProcessor()->GetModel(m_meshHandle);
+            if (model)
+            {
+                const char* NoLodOverrideText = "No LOD Override";
+                const char* LodFormatString = "LOD %i";
+
+                AZStd::string previewText = m_lodOverride == AZ::RPI::Cullable::NoLodOverride ? NoLodOverrideText : AZStd::string::format(LodFormatString, m_lodOverride);
+
+                if (ScriptableImGui::BeginCombo("", previewText.c_str()))
+                {
+                    if (ScriptableImGui::Selectable(NoLodOverrideText, m_lodOverride == AZ::RPI::Cullable::NoLodOverride))
+                    {
+                        m_lodOverride = AZ::RPI::Cullable::NoLodOverride;
+                        GetMeshFeatureProcessor()->SetLodOverride(m_meshHandle, m_lodOverride);
+                    }
+
+                    for (uint32_t i = 0; i < model->GetLodCount(); ++i)
+                    {
+                        AZStd::string name = AZStd::string::format(LodFormatString, i);
+                        if (ScriptableImGui::Selectable(name.c_str(), m_lodOverride == i))
+                        {
+                            m_lodOverride = i;
+                            GetMeshFeatureProcessor()->SetLodOverride(m_meshHandle, m_lodOverride);
+                        }
+                    }
+                    ScriptableImGui::EndCombo();
+                }
+            }
+
+            ImGui::Spacing();
+            ImGui::Separator();
+            ImGui::Spacing();
+
+            // Camera controls
+            {
+                int32_t* currentControllerTypeIndex = reinterpret_cast<int32_t*>(&m_currentCameraControllerType);
+
+                ImGui::LabelText("##CameraControllerLabel", "Camera Controller:");
+                if (ScriptableImGui::Combo("##CameraController", currentControllerTypeIndex, CameraControllerNameTable, CameraControllerCount))
+                {
+                    ResetCameraController();
+                }
+            }
+
+            ImGui::Spacing();
+            ImGui::Separator();
+            ImGui::Spacing();
+
+            if (m_materialOverrideInstance && ImGui::Button("Material Details..."))
+            {
+                m_imguiMaterialDetails.SetMaterial(m_materialOverrideInstance);
+                m_imguiMaterialDetails.OpenDialog();
+            }
+
+            m_imguiSidebar.End();
+        }
+
+        m_imguiMaterialDetails.Tick();
+
+        if (modelNeedsUpdate)
+        {
+            ModelChange();
+        }
+    }
+
+    void MeshExampleComponent::ModelChange()
+    {
+        GetMeshFeatureProcessor()->ReleaseMesh(m_meshHandle);
+
+        if (!m_modelBrowser.GetSelectedAssetId().IsValid())
+        {
+            return;
+        }
+
+        // If a material hasn't been selected, just choose the first one
+        // If for some reason no materials are available log an error
+        AZ::Data::AssetId selectedMaterialAssetId = m_materialBrowser.GetSelectedAssetId();
+        if (!selectedMaterialAssetId.IsValid())
+        {
+            selectedMaterialAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(DefaultPbrMaterialPath, AZ::RPI::AssetUtils::TraceLevel::Error);
+
+            if (!selectedMaterialAssetId.IsValid())
+            {
+                AZ_Error("MeshExampleComponent", false, "Failed to select model, no material available to render with.");
+                return;
+            }
+        }
+
+        AZ::Render::MaterialAssignmentMap materialMap;
+        if (m_enableMaterialOverride && selectedMaterialAssetId.IsValid())
+        {
+            AZ::Data::Asset<AZ::RPI::MaterialAsset> materialAsset;
+            materialAsset.Create(selectedMaterialAssetId);
+
+            m_materialOverrideInstance = AZ::RPI::Material::FindOrCreate(materialAsset);
+
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialAsset = materialAsset;
+            materialMap[AZ::Render::DefaultMaterialAssignmentId].m_materialInstance = m_materialOverrideInstance;
+        }
+        else
+        {
+            m_materialOverrideInstance = nullptr;
+        }
+
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScript);
+
+        AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset;
+        modelAsset.Create(m_modelBrowser.GetSelectedAssetId());
+        m_meshHandle = GetMeshFeatureProcessor()->AcquireMesh(modelAsset, materialMap);
+        GetMeshFeatureProcessor()->SetTransform(m_meshHandle, AZ::Transform::CreateIdentity());
+        GetMeshFeatureProcessor()->ConnectModelChangeEventHandler(m_meshHandle, m_changedHandler);
+        GetMeshFeatureProcessor()->SetLodOverride(m_meshHandle, m_lodOverride);
+    }
+
+    void MeshExampleComponent::OnEntityDestruction(const AZ::EntityId& entityId)
+    {
+        OnLightingPresetEntityShutdown(entityId);
+        AZ::EntityBus::MultiHandler::BusDisconnect(entityId);
+    }
+
+    void MeshExampleComponent::UseArcBallCameraController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::ArcBallControllerComponent>());
+    }
+
+    void MeshExampleComponent::UseNoClipCameraController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+    }
+
+    void MeshExampleComponent::RemoveController()
+    {
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+    }
+
+    void MeshExampleComponent::SetArcBallControllerParams()
+    {
+        if (!m_modelBrowser.GetSelectedAssetId().IsValid())
+        {
+            return;
+        }
+
+        // Adjust the arc-ball controller so that it has bounds that make sense for the current model
+        AZ::Data::Asset<AZ::RPI::ModelAsset> asset = AZ::Data::AssetManager::Instance().GetAsset(m_modelBrowser.GetSelectedAssetId(), azrtti_typeid<AZ::RPI::ModelAsset>(),
+            AZ::Data::AssetLoadBehavior::PreLoad);
+        asset.BlockUntilLoadComplete();
+
+        AZ::RPI::ModelAsset* modelAsset = asset.Get();
+        const AZ::Aabb& aabb = modelAsset->GetAabb();
+
+        AZ::Vector3 center;
+        float radius;
+        aabb.GetAsSphere(center, radius);
+
+        const float startingDistance = radius;
+        const float minDistance = radius * ArcballRadiusMinModifier;
+        const float maxDistance = radius * ArcballRadiusMaxModifier;
+
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetCenter, center);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetDistance, startingDistance);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetMinDistance, minDistance);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetMaxDistance, maxDistance);
+    }
+    void MeshExampleComponent::ResetCameraController()
+    {
+        RemoveController();
+        if (m_currentCameraControllerType == CameraControllerType::ArcBall)
+        {
+            UseArcBallCameraController();
+            SetArcBallControllerParams();
+        }
+        else if (m_currentCameraControllerType == CameraControllerType::NoClip)
+        {
+            UseNoClipCameraController();
+        }
+    }
+} // namespace AtomSampleViewer

+ 99 - 0
Gem/Code/Source/MeshExampleComponent.h

@@ -0,0 +1,99 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+
+#include <AzCore/Component/EntityBus.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <Utils/Utils.h>
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/ImGuiMaterialDetails.h>
+#include <Utils/ImGuiAssetBrowser.h>
+
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+
+namespace AtomSampleViewer
+{
+    class MeshExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(MeshExampleComponent, "{A2402165-5DF1-4981-BF7F-665209640BBD}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        MeshExampleComponent();
+        ~MeshExampleComponent() override = default;
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        // AZ::TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+        // AZ::EntityBus::MultiHandler
+        void OnEntityDestruction(const AZ::EntityId& entityId) override;
+
+        void ModelChange();
+
+        void UseArcBallCameraController();
+        void UseNoClipCameraController();
+        void RemoveController();
+
+        void SetArcBallControllerParams();
+        void ResetCameraController();
+
+        enum class CameraControllerType : int32_t 
+        {
+            ArcBall = 0,
+            NoClip,
+            Count
+        };
+        static const uint32_t CameraControllerCount = static_cast<uint32_t>(CameraControllerType::Count);
+        static const char* CameraControllerNameTable[CameraControllerCount];
+        CameraControllerType m_currentCameraControllerType = CameraControllerType::ArcBall;
+
+        // Not owned by this sample, we look this up
+        AZ::Component* m_cameraControlComponent = nullptr;
+
+        AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler m_changedHandler;
+
+        static constexpr float ArcballRadiusMinModifier = 0.01f;
+        static constexpr float ArcballRadiusMaxModifier = 4.0f;
+        
+        AZ::RPI::Cullable::LodOverride m_lodOverride = AZ::RPI::Cullable::NoLodOverride;
+
+        bool m_enableMaterialOverride = true;
+
+        // If false, only azmaterials generated from ".material" files will be listed.
+        // Otherwise, all azmaterials, regardless of its source (e.g ".fbx"), will
+        // be shown in the material list.
+        bool m_showModelMaterials = false;
+
+        bool m_cameraControllerDisabled = false;
+
+        AZ::Data::Instance<AZ::RPI::Material> m_materialOverrideInstance; //< Holds a copy of the material instance being used when m_enableMaterialOverride is true.
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+
+        ImGuiSidebar m_imguiSidebar;
+        ImGuiMaterialDetails m_imguiMaterialDetails;
+        ImGuiAssetBrowser m_materialBrowser;
+        ImGuiAssetBrowser m_modelBrowser;
+    };
+} // namespace AtomSampleViewer

+ 609 - 0
Gem/Code/Source/MultiRenderPipelineExampleComponent.cpp

@@ -0,0 +1,609 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <AtomSampleViewerOptions.h>
+#include <MultiRenderPipelineExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/CameraComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/RHI/RHISystemInterface.h>
+
+#include <Atom/RPI.Public/ViewProviderBus.h>
+#include <Atom/RPI.Public/RenderPipeline.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+
+#include <Atom/Feature/ImGui/ImGuiUtils.h>
+
+#include <AzCore/Math/MatrixUtils.h>
+
+#include <Automation/ScriptableImGui.h>
+#include <Automation/ScriptRunnerBus.h>
+
+#include <AzCore/Component/Entity.h>
+
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Scene/SceneSystemBus.h>
+#include <AzFramework/Entity/GameEntityContextComponent.h>
+
+#include <EntityUtilityFunctions.h>
+#include <SampleComponentConfig.h>
+#include <SampleComponentManager.h>
+
+#include <Utils/Utils.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    using namespace AZ;
+
+    namespace
+    {
+        const char* BunnyModelFilePath = "objects/bunny.azmodel";
+        const char* CubeModelFilePath = "testdata/objects/cube/cube.azmodel";
+    };
+
+    void MultiRenderPipelineExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MultiRenderPipelineExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    MultiRenderPipelineExampleComponent::MultiRenderPipelineExampleComponent()
+        : m_imguiSidebar("@user@/MultiRenderPipelineExampleComponent/sidebar.xml")
+    {
+        m_sampleName = "MultiRenderPipelineExampleComponent";
+    }
+
+    void MultiRenderPipelineExampleComponent::Activate()
+    {
+        m_scene = RPI::RPISystemInterface::Get()->GetDefaultScene();
+
+        // Save references of different feature processors
+        m_spotLightFeatureProcessor = m_scene->GetFeatureProcessor<Render::SpotLightFeatureProcessorInterface>();
+        m_directionalLightFeatureProcessor = m_scene->GetFeatureProcessor<Render::DirectionalLightFeatureProcessorInterface>();
+        m_skyboxFeatureProcessor = m_scene->GetFeatureProcessor<AZ::Render::SkyBoxFeatureProcessorInterface>();
+        m_postProcessFeatureProcessor = m_scene->GetFeatureProcessor<AZ::Render::PostProcessFeatureProcessorInterface>();
+
+        m_ibl.PreloadAssets();
+
+        // Don't continue the script until assets are ready and scene is setup
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::PauseScriptWithTimeout, 120.0f);
+
+        // preload assets
+        AZStd::vector<AssetCollectionAsyncLoader::AssetToLoadInfo> assetList = {
+            {DefaultPbrMaterialPath, azrtti_typeid<RPI::MaterialAsset>()},
+            {BunnyModelFilePath, azrtti_typeid<RPI::ModelAsset>()},
+            {CubeModelFilePath, azrtti_typeid<RPI::ModelAsset>()}
+        };
+
+        PreloadAssets(assetList);
+    }
+
+    void MultiRenderPipelineExampleComponent::OnAllAssetsReadyActivate()
+    {
+        SetupScene();
+            
+        if (SupportsMultipleWindows() && m_enableSecondRenderPipeline)
+        {
+            AddSecondRenderPipeline();
+        }
+
+        AZ::TickBus::Handler::BusConnect();
+        m_imguiSidebar.Activate();
+        ScriptRunnerRequestBus::Broadcast(&ScriptRunnerRequests::ResumeScript);
+    }
+
+    void MultiRenderPipelineExampleComponent::SetupScene()
+    {
+        auto meshFeatureProcessor = GetMeshFeatureProcessor();
+
+        auto materialAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::MaterialAsset>(DefaultPbrMaterialPath,
+            RPI::AssetUtils::TraceLevel::Assert);
+        auto material = AZ::RPI::Material::FindOrCreate(materialAsset);
+        auto bunnyAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::ModelAsset>(BunnyModelFilePath,
+            RPI::AssetUtils::TraceLevel::Assert);
+        auto floorAsset = RPI::AssetUtils::LoadAssetByProductPath<RPI::ModelAsset>(CubeModelFilePath,
+            RPI::AssetUtils::TraceLevel::Assert);
+        m_floorMeshHandle = meshFeatureProcessor->AcquireMesh(floorAsset, material);
+        for (uint32_t bunnyIndex = 0; bunnyIndex < BunnyCount; bunnyIndex++)
+        {
+            m_bunnyMeshHandles[bunnyIndex] = meshFeatureProcessor->AcquireMesh(bunnyAsset, material);
+        }
+
+        const Vector3 scale{ 12.f, 12.f, 0.1f };
+        const Vector3 translation{ 0.f, 0.f, -0.05f };
+        Transform floorTransform = Transform::CreateTranslation(translation) * AZ::Transform::CreateScale(scale);
+        meshFeatureProcessor->SetTransform(m_floorMeshHandle, floorTransform);
+
+        meshFeatureProcessor->SetTransform(m_bunnyMeshHandles[0], Transform::CreateTranslation(Vector3(0.f, 0.f, 0.21f)));
+        meshFeatureProcessor->SetTransform(m_bunnyMeshHandles[1], Transform::CreateTranslation(Vector3(-3.f, 3.f, 0.21f)));
+        meshFeatureProcessor->SetTransform(m_bunnyMeshHandles[2], Transform::CreateTranslation(Vector3(3.f, 3.f, 0.21f)));
+        meshFeatureProcessor->SetTransform(m_bunnyMeshHandles[3], Transform::CreateTranslation(Vector3(3.f, -3.f, 0.21f)));
+        meshFeatureProcessor->SetTransform(m_bunnyMeshHandles[4], Transform::CreateTranslation(Vector3(-3.f, -3.f, 0.21f)));
+
+        // Set camera to use no clip controller
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+        AZ::Debug::NoClipControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::NoClipControllerRequestBus::Events::SetFov, AZ::DegToRad(90));
+
+        // Set camera's initial transform
+        Transform cameraLookat = Transform::CreateLookAt(Vector3(-2.15f, -5.25f, 1.6f), Vector3(-0.5f, -2.0f, 0));
+        AZ::TransformBus::Event(GetCameraEntityId(), &AZ::TransformInterface::SetWorldTM, cameraLookat);
+
+        if (m_enabledDepthOfField)
+        {
+            EnableDepthOfField();
+        }
+        if (m_hasDirectionalLight)
+        {
+            AddDirectionalLight();
+        }
+        if (m_hasSpotLight)
+        {
+            AddSpotLight();
+        }
+        if (m_enabledSkybox)
+        {
+            EnableSkybox();
+        }
+        if (m_hasIBL)
+        {
+            AddIBL();
+        }
+    }
+
+    void MultiRenderPipelineExampleComponent::EnableDepthOfField()
+    {
+        if (m_postProcessFeatureProcessor)
+        {
+            auto* postProcessSettings = m_postProcessFeatureProcessor->GetOrCreateSettingsInterface(GetCameraEntityId());
+            auto* dofSettings = postProcessSettings->GetOrCreateDepthOfFieldSettingsInterface();
+            dofSettings->SetQualityLevel(1);
+            dofSettings->SetApertureF(0.5f);
+            dofSettings->SetEnableDebugColoring(false);
+            dofSettings->SetEnableAutoFocus(true);
+            dofSettings->SetAutoFocusScreenPosition(AZ::Vector2{ 0.5f, 0.5f });
+            dofSettings->SetAutoFocusSensitivity(1.0f);
+            dofSettings->SetAutoFocusSpeed(Render::DepthOfField::AutoFocusSpeedMax);
+            dofSettings->SetAutoFocusDelay(0.2f);
+            dofSettings->SetCameraEntityId(GetCameraEntityId());
+            dofSettings->SetEnabled(true);
+            dofSettings->OnConfigChanged();
+
+            // associate a setting to the primary view
+            AZ::RPI::ViewPtr cameraView;
+            AZ::RPI::ViewProviderBus::EventResult(cameraView, GetCameraEntityId(), &AZ::RPI::ViewProvider::GetView);
+            AZ::Render::PostProcessSettingsInterface::ViewBlendWeightMap perViewBlendWeights;
+            perViewBlendWeights[cameraView.get()] = 1.0;
+            postProcessSettings->CopyViewToBlendWeightSettings(perViewBlendWeights);
+        }
+    }
+
+    void MultiRenderPipelineExampleComponent::DisableDepthOfField()
+    {
+        if (m_postProcessFeatureProcessor)
+        {
+            auto* postProcessSettings = m_postProcessFeatureProcessor->GetOrCreateSettingsInterface(GetCameraEntityId());
+            auto* dofSettings = postProcessSettings->GetOrCreateDepthOfFieldSettingsInterface();
+            dofSettings->SetEnabled(false);
+            dofSettings->OnConfigChanged();
+        }
+    }
+
+    void MultiRenderPipelineExampleComponent::AddDirectionalLight()
+    {
+        if (m_directionalLightHandle.IsValid())
+        {
+            return;
+        }
+
+        auto& featureProcessor = m_directionalLightFeatureProcessor;
+
+        DirectionalLightHandle handle = featureProcessor->AcquireLight();
+
+        const auto lightTransform = Transform::CreateLookAt(
+            Vector3(100, 100, 100),
+            Vector3::CreateZero());
+
+        featureProcessor->SetDirection(handle, lightTransform.GetBasis(1));
+        AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Lux> lightColor(Color::CreateOne() * 5.0f);
+        featureProcessor->SetRgbIntensity(handle, lightColor);
+        featureProcessor->SetCascadeCount(handle, 4);
+        featureProcessor->SetShadowmapSize(handle, Render::ShadowmapSize::Size2048);
+        featureProcessor->SetViewFrustumCorrectionEnabled(handle, true);
+        featureProcessor->SetShadowFilterMethod(handle, aznumeric_cast<Render::ShadowFilterMethod>(m_shadowFilteringMethod));
+        featureProcessor->SetShadowBoundaryWidth(handle, 0.03f);
+        featureProcessor->SetPredictionSampleCount(handle, 4);
+        featureProcessor->SetFilteringSampleCount(handle, 32);
+        featureProcessor->SetGroundHeight(handle, 0.f);
+        featureProcessor->SetShadowFarClipDistance(handle, 100.f);
+
+        m_directionalLightHandle = handle;
+    }
+
+    void MultiRenderPipelineExampleComponent::RemoveDirectionalLight()
+    {
+        if (m_directionalLightHandle.IsValid())
+        {
+            m_directionalLightFeatureProcessor->ReleaseLight(m_directionalLightHandle);
+        }
+    }
+
+    void MultiRenderPipelineExampleComponent::AddSpotLight()
+    {
+        Render::SpotLightFeatureProcessorInterface* const featureProcessor = m_spotLightFeatureProcessor;
+
+        const SpotLightHandle handle = featureProcessor->AcquireLight();
+
+        AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Candela> lightColor(Colors::Green * 500.0f);
+        featureProcessor->SetRgbIntensity(handle, lightColor);
+        featureProcessor->SetAttenuationRadius(handle, 30.0f);
+        featureProcessor->SetConeAngles(handle, 35*0.9f, 35);
+        featureProcessor->SetShadowmapSize(handle, Render::ShadowmapSize::Size1024);
+        Vector3 position(0, 5, 7);
+        Vector3 direction = -position;
+        direction.Normalize();
+        featureProcessor->SetPosition(handle, position);
+        featureProcessor->SetDirection(handle, direction);
+
+        m_spotLightHandle = handle;
+    }
+
+    void MultiRenderPipelineExampleComponent::RemoveSpotLight()
+    {
+        if (m_spotLightHandle.IsValid())
+        {
+            m_spotLightFeatureProcessor->ReleaseLight(m_spotLightHandle);
+        }
+    }
+
+    void MultiRenderPipelineExampleComponent::EnableSkybox()
+    {
+        m_skyboxFeatureProcessor->SetSkyboxMode(AZ::Render::SkyBoxMode::PhysicalSky);
+        m_skyboxFeatureProcessor->Enable(true);
+    }
+
+    void MultiRenderPipelineExampleComponent::DisableSkybox()
+    {
+        if (m_skyboxFeatureProcessor)
+        {
+            m_skyboxFeatureProcessor->Enable(false);
+        }
+    }
+    
+    void MultiRenderPipelineExampleComponent::AddIBL()
+    {
+        m_ibl.Init(m_scene.get());
+    }
+
+    void MultiRenderPipelineExampleComponent::RemoveIBL()
+    {
+        m_ibl.Reset();
+    }
+    
+    void MultiRenderPipelineExampleComponent::AddSecondRenderPipeline()
+    {
+        // Create second render pipeline
+        // Create a second window for this render pipeline
+        m_secondWindow = AZStd::make_unique<AzFramework::NativeWindow>("Multi render pipeline: Second Window",
+            AzFramework::WindowGeometry(0, 0, 800, 600));
+        m_secondWindow->Activate();
+        RHI::Ptr<RHI::Device> device = RHI::RHISystemInterface::Get()->GetDevice();
+        m_secondWindowContext = AZStd::make_shared<RPI::WindowContext>();
+        m_secondWindowContext->Initialize(*device, m_secondWindow->GetWindowHandle());
+
+        AzFramework::WindowNotificationBus::Handler::BusConnect(m_secondWindow->GetWindowHandle());
+
+        // Create a custom pipeline descriptor
+        AZ::RPI::RenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.m_mainViewTagName = "MainCamera";
+        pipelineDesc.m_name = "SecondPipeline";
+        pipelineDesc.m_rootPassTemplate = "MainPipeline";
+        pipelineDesc.m_renderSettings.m_multisampleState.m_samples = 4;
+        m_secondPipeline = AZ::RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_secondWindowContext);
+
+        m_scene->AddRenderPipeline(m_secondPipeline);
+
+        // Create a new camera entity and use it for the second render pipeline
+        m_secondViewCameraEntity = AtomSampleViewer::CreateEntity("SecondViewCamera", GetEntityContextId());
+
+        AZ::Debug::CameraComponentConfig cameraConfig(m_secondWindowContext);
+        cameraConfig.m_fovY = AZ::Constants::QuarterPi * 1.5f;
+        cameraConfig.m_target = m_secondWindowContext;
+        AZ::Debug::CameraComponent* camComponent = static_cast<AZ::Debug::CameraComponent*>(m_secondViewCameraEntity->CreateComponent(
+            azrtti_typeid<AZ::Debug::CameraComponent>()));
+        camComponent->SetConfiguration(cameraConfig);
+        m_secondViewCameraEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_secondViewCameraEntity->CreateComponent(azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+        m_secondViewCameraEntity->Activate();
+        AZ::Debug::CameraControllerRequestBus::Event(m_secondViewCameraEntity->GetId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+
+        // Set camera's initial transform and fov
+        auto cameraId = m_secondViewCameraEntity->GetId();
+        AZ::Debug::NoClipControllerRequestBus::Event(cameraId, &AZ::Debug::NoClipControllerRequestBus::Events::SetFov, AZ::DegToRad(90));
+        AZ::Debug::NoClipControllerRequestBus::Event(cameraId, &AZ::Debug::NoClipControllerRequestBus::Events::SetPosition, Vector3(2.75f, 5.25f, 1.49f));
+        AZ::Debug::NoClipControllerRequestBus::Event(cameraId, &AZ::Debug::NoClipControllerRequestBus::Events::SetHeading, AZ::DegToRad(-171.9));
+        AZ::Debug::NoClipControllerRequestBus::Event(cameraId, &AZ::Debug::NoClipControllerRequestBus::Events::SetPitch, AZ::DegToRad(-11.6f));
+
+        if (m_useSecondCamera)
+        {
+            AZ::EntityId secondCameraEntityId = m_secondViewCameraEntity->GetId();
+            m_secondPipeline->SetDefaultViewFromEntity(secondCameraEntityId);
+
+            // associate a setting to the secondary view
+            AZ::RPI::ViewPtr cameraView;
+            AZ::RPI::ViewProviderBus::EventResult(cameraView, secondCameraEntityId, &AZ::RPI::ViewProvider::GetView);
+            AZ::Render::PostProcessSettingsInterface::ViewBlendWeightMap perViewBlendWeights;
+            perViewBlendWeights[cameraView.get()] = 1.0;
+
+            auto* postProcessSettings = m_postProcessFeatureProcessor->GetOrCreateSettingsInterface(secondCameraEntityId);
+            postProcessSettings->CopyViewToBlendWeightSettings(perViewBlendWeights);
+        }
+        else
+        {
+            m_secondPipeline->SetDefaultViewFromEntity(GetCameraEntityId());
+        }
+    }
+
+    void MultiRenderPipelineExampleComponent::RemoveSecondRenderPipeline()
+    {
+        AzFramework::WindowNotificationBus::Handler::BusDisconnect();
+        if (m_secondViewCameraEntity)
+        {
+            DestroyEntity(m_secondViewCameraEntity);
+            m_secondViewCameraEntity = nullptr;
+        }
+        
+        if (m_secondPipeline)
+        {
+            m_secondPipeline->RemoveFromScene();
+            m_secondPipeline = nullptr;
+        }
+        m_secondWindowContext = nullptr;
+        m_secondWindow = nullptr;
+    }
+
+    void MultiRenderPipelineExampleComponent::CleanUpScene()
+    {
+        RemoveIBL();
+        DisableSkybox();
+        RemoveSpotLight();
+        RemoveDirectionalLight();
+        DisableDepthOfField();
+
+        RemoveSecondRenderPipeline();
+
+        GetMeshFeatureProcessor()->ReleaseMesh(m_floorMeshHandle);
+        for (auto index = 0; index < BunnyCount; index++)
+        {
+            GetMeshFeatureProcessor()->ReleaseMesh(m_bunnyMeshHandles[index]);
+        }
+    }
+
+    void MultiRenderPipelineExampleComponent::Deactivate()
+    {
+        m_imguiSidebar.Deactivate();
+        AZ::TickBus::Handler::BusDisconnect();
+
+        CleanUpScene();
+
+        m_directionalLightFeatureProcessor = nullptr;
+        m_spotLightFeatureProcessor = nullptr;
+        m_skyboxFeatureProcessor = nullptr;
+        m_scene = nullptr;
+    }
+
+    void MultiRenderPipelineExampleComponent::OnWindowClosed()
+    {
+        RemoveSecondRenderPipeline();
+        m_enableSecondRenderPipeline = false;
+    }
+
+    void MultiRenderPipelineExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        bool queueSecondWindowDeactivate = false;
+
+        if (m_hasDirectionalLight)
+        {
+            auto& featureProcessor = m_directionalLightFeatureProcessor;
+            Transform cameraTransform = Transform::CreateIdentity();
+            TransformBus::EventResult(
+                cameraTransform,
+                GetCameraEntityId(),
+                &TransformBus::Events::GetWorldTM);
+            featureProcessor->SetCameraTransform(
+                m_directionalLightHandle,
+                cameraTransform);
+
+            if (m_useSecondCamera && m_secondViewCameraEntity)
+            {
+                TransformBus::EventResult(
+                    cameraTransform,
+                    m_secondViewCameraEntity->GetId(),
+                    &TransformBus::Events::GetWorldTM);
+                featureProcessor->SetCameraTransform(
+                    m_directionalLightHandle,
+                    cameraTransform,
+                    m_secondPipeline->GetId());
+            }
+        }
+
+        if (m_imguiSidebar.Begin())
+        {
+
+            ImGui::Spacing();
+
+            ImGui::Text("Second RenderPipeline");
+            ImGui::Indent();
+
+            if (SupportsMultipleWindows())
+            {
+                if (ScriptableImGui::Checkbox("Enable second pipeline", &m_enableSecondRenderPipeline))
+                {
+                    if (m_enableSecondRenderPipeline)
+                    {
+                        AddSecondRenderPipeline();
+                    }
+                    else
+                    {
+                        queueSecondWindowDeactivate = true;
+                    }
+                }
+                ImGui::Indent();
+                if (m_secondPipeline)
+                {
+                    if (ScriptableImGui::Checkbox("Use second camera", &m_useSecondCamera))
+                    {
+                        if (m_useSecondCamera)
+                        {
+                            m_secondPipeline->SetDefaultViewFromEntity(m_secondViewCameraEntity->GetId());
+                        }
+                        else
+                        {
+                            m_secondPipeline->SetDefaultViewFromEntity(GetCameraEntityId());
+                        }
+                    }
+                }
+                ImGui::Unindent();
+                ImGui::Separator();
+            }
+            ImGui::Unindent();
+
+            ImGui::Spacing();
+
+            ImGui::Text("Features");
+            ImGui::Indent();
+            if (ScriptableImGui::Checkbox("Enable Depth of Field", &m_enabledDepthOfField))
+            {
+                if (m_enabledDepthOfField)
+                {
+                    EnableDepthOfField();
+                }
+                else
+                {
+                    DisableDepthOfField();
+                }
+            }
+
+            if (ScriptableImGui::Checkbox("Add/Remove Directional Light", &m_hasDirectionalLight))
+            {
+                if (m_hasDirectionalLight)
+                {
+                    AddDirectionalLight();
+                }
+                else
+                {
+                    RemoveDirectionalLight();
+                }
+            }
+            if (m_hasDirectionalLight)
+            {
+                ImGui::Indent();
+                ImGui::Text("Filtering Method");
+                bool methodChanged = false;
+                methodChanged = methodChanged ||
+                    ScriptableImGui::RadioButton("None", &m_shadowFilteringMethod, 0);
+                ImGui::SameLine();
+                methodChanged = methodChanged ||
+                    ScriptableImGui::RadioButton("PCF", &m_shadowFilteringMethod, 1);
+                ImGui::SameLine();
+                methodChanged = methodChanged ||
+                    ScriptableImGui::RadioButton("ESM", &m_shadowFilteringMethod, 2);
+                ImGui::SameLine();
+                methodChanged = methodChanged ||
+                    ScriptableImGui::RadioButton("ESM+PCF", &m_shadowFilteringMethod, 3);
+                if (methodChanged)
+                {
+                    m_directionalLightFeatureProcessor->SetShadowFilterMethod(m_directionalLightHandle, aznumeric_cast<Render::ShadowFilterMethod>(m_shadowFilteringMethod));
+                }
+                ImGui::Unindent();
+            }
+
+            if (ScriptableImGui::Checkbox("Add/Remove Spot Light", &m_hasSpotLight))
+            {
+                if (m_hasSpotLight)
+                {
+                    AddSpotLight();
+                }
+                else
+                {
+                    RemoveSpotLight();
+                }
+            }
+
+            if (ScriptableImGui::Checkbox("Enable Skybox", &m_enabledSkybox))
+            {
+                if (m_enabledSkybox)
+                {
+                    EnableSkybox();
+                }
+                else
+                {
+                    DisableSkybox();
+                }
+            }
+
+            if (ScriptableImGui::Checkbox("Add/Remove IBL", &m_hasIBL))
+            {
+                if (m_hasIBL)
+                {
+                    AddIBL();
+                }
+                else
+                {
+                    RemoveIBL();
+                }
+            }
+
+            ImGui::Unindent();
+
+            ImGui::Separator();
+
+            m_imguiSidebar.End();
+        }
+
+        if (m_secondPipeline)
+        {
+            Render::ImGuiActiveContextScope contextGuard = Render::ImGuiActiveContextScope::FromPass(RPI::PassHierarchyFilter({ "SecondPipeline", "ImGuiPass"}));
+            AZ_Error("MultiRenderPipelineExampleComponent", contextGuard.IsEnabled(), "Unable to activate imgui context for second pipeline.");
+
+            if (contextGuard.IsEnabled())
+            {
+                if (ImGui::Begin("Second Window"))
+                {
+                    if (ScriptableImGui::Button("Close Window"))
+                    {
+                        queueSecondWindowDeactivate = true;
+                    }
+                }
+                ImGui::End();
+            }
+        }
+
+        if (queueSecondWindowDeactivate)
+        {
+            m_secondWindow->Deactivate();
+        }
+    }
+        
+
+} // namespace AtomSampleViewer

+ 144 - 0
Gem/Code/Source/MultiRenderPipelineExampleComponent.h

@@ -0,0 +1,144 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <Atom/RPI.Public/Base.h>
+#include <Atom/RPI.Public/WindowContext.h>
+
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <AzFramework/Windowing/WindowBus.h>
+#include <AzFramework/Windowing/NativeWindow.h>
+
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/SpotLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/ShadowConstants.h>
+#include <Atom/Feature/SkyBox/SkyBoxFeatureProcessorInterface.h>
+#include <Atom/Feature/PostProcess/PostProcessFeatureProcessorInterface.h>
+
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/Utils.h>
+
+struct ImGuiContext;
+
+namespace AtomSampleViewer
+{
+    //! A sample component which render the same scene with different render pipelines in different windows
+    //! It has a imgui menu to switch on/off the second render pipeline as well as turn on/off different graphics features
+    //! There is also an option to have the second render pipeline to use the second camera. 
+    class MultiRenderPipelineExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+        , public AzFramework::WindowNotificationBus::Handler
+    {
+    public:
+        AZ_COMPONENT(MultiRenderPipelineExampleComponent, "{A3654684-DB33-4B2C-B7AB-9B1D6BF3FCF1}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        MultiRenderPipelineExampleComponent();
+        ~MultiRenderPipelineExampleComponent() final = default;
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+        // AzFramework::WindowNotificationBus::Handler
+        void OnWindowClosed() override;
+        
+    private:
+        // AZ::TickBus::Handler overrides ...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        // CommonSampleComponentBase overrides...
+        void OnAllAssetsReadyActivate() override;
+
+        // Add render content to the scene
+        void SetupScene();
+        // Clean up render content in the scene
+        void CleanUpScene();
+
+        // enable/disable different features for the second render pipeline 
+        void EnableDepthOfField();
+        void DisableDepthOfField();
+
+        void AddDirectionalLight();
+        void RemoveDirectionalLight();
+
+        void AddSpotLight();
+        void RemoveSpotLight();
+
+        void EnableSkybox();
+        void DisableSkybox();
+
+        void AddIBL();
+        void RemoveIBL();
+        
+        void AddSecondRenderPipeline();
+        void RemoveSecondRenderPipeline();
+        
+        // For draw menus of selecting pipelines
+        ImGuiSidebar m_imguiSidebar;
+        
+        // For scene content
+        using DirectionalLightHandle = AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle;
+        using SpotLightHandle = AZ::Render::SpotLightFeatureProcessorInterface::LightHandle;
+        using MeshHandle = AZ::Render::MeshFeatureProcessorInterface::MeshHandle;
+
+        // Directional light
+        AZ::Render::DirectionalLightFeatureProcessorInterface* m_directionalLightFeatureProcessor = nullptr;
+        DirectionalLightHandle m_directionalLightHandle;
+        // Spot light
+        AZ::Render::SpotLightFeatureProcessorInterface* m_spotLightFeatureProcessor = nullptr;
+        SpotLightHandle m_spotLightHandle;
+        // Meshes
+        MeshHandle m_floorMeshHandle;
+
+        static const uint32_t BunnyCount = 5;
+        MeshHandle m_bunnyMeshHandles[BunnyCount];
+        // Skybox
+        AZ::Render::SkyBoxFeatureProcessorInterface* m_skyboxFeatureProcessor = nullptr;
+        // IBL
+        Utils::DefaultIBL m_ibl;
+        // Post Process
+        AZ::Render::PostProcessFeatureProcessorInterface* m_postProcessFeatureProcessor = nullptr;
+        AZ::Entity* m_depthOfFieldEntity = nullptr;
+
+        // flags of features enabled
+        bool m_enabledDepthOfField = true;
+        bool m_hasDirectionalLight = true;
+        bool m_hasSpotLight = true;
+        bool m_enabledSkybox = true;
+        bool m_hasIBL = true;
+        bool m_enableSecondRenderPipeline = true;
+        bool m_useSecondCamera = false;
+
+        // for directional light
+        int m_shadowFilteringMethod = aznumeric_cast<int>(AZ::Render::ShadowFilterMethod::EsmPcf);
+
+        // For second render pipeline
+        AZStd::shared_ptr<AzFramework::NativeWindow> m_secondWindow;
+        AZStd::shared_ptr<AZ::RPI::WindowContext> m_secondWindowContext;
+        AZ::RPI::RenderPipelinePtr m_secondPipeline;
+
+        // camera for the second render pipeline
+        AZ::Entity* m_secondViewCameraEntity = nullptr;
+
+        // Reference to current scene
+        AZ::RPI::ScenePtr m_scene;
+    };
+
+} // namespace AtomSampleViewer

+ 518 - 0
Gem/Code/Source/MultiSceneExampleComponent.cpp

@@ -0,0 +1,518 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <AtomSampleViewerOptions.h>
+#include <MultiSceneExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/CameraComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/RPI.Public/RenderPipeline.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RHI/RHISystemInterface.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+
+#include <AzCore/Math/MatrixUtils.h>
+
+#include <AzCore/Component/Entity.h>
+
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Scene/SceneSystemBus.h>
+#include <AzFramework/Entity/GameEntityContextComponent.h>
+
+#include <SampleComponentConfig.h>
+#include <SampleComponentManager.h>
+#include <EntityUtilityFunctions.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    //////////////////////////////////////////////////////////////////////////
+    // SecondWindowedScene
+
+    SecondWindowedScene::SecondWindowedScene(AZStd::string_view sceneName, MultiSceneExampleComponent* parent)
+    {
+        using namespace AZ;
+
+        m_sceneName = sceneName;
+        m_parent = parent;
+
+        // Create a new EntityContext and AzFramework::Scene, and link them together via SetSceneForEntityContextId
+        m_entityContext = AZStd::make_unique<AzFramework::EntityContext>();
+        m_entityContext->InitContext();
+
+        // Create the scene
+        Outcome<AzFramework::Scene*, AZStd::string> createSceneOutcome = Failure<AZStd::string>("SceneSystemRequests bus not responding.");
+        AzFramework::SceneSystemRequestBus::BroadcastResult(createSceneOutcome, &AzFramework::SceneSystemRequests::CreateScene, m_sceneName);
+        AZ_Assert(createSceneOutcome, "%s", createSceneOutcome.GetError().data());
+        m_frameworkScene = createSceneOutcome.GetValue();
+        bool success = false;
+        AzFramework::SceneSystemRequestBus::BroadcastResult(success, &AzFramework::SceneSystemRequests::SetSceneForEntityContextId, m_entityContext->GetContextId(), m_frameworkScene);
+        AZ_Assert(success, "Unable to set entity context on AzFramework::Scene: %s", m_sceneName.c_str());
+
+        // Create a NativeWindow and WindowContext
+        m_nativeWindow = AZStd::make_unique<AzFramework::NativeWindow>("Multi Scene: Second Window", AzFramework::WindowGeometry(0, 0, 1280, 720));
+        m_nativeWindow->Activate();
+        RHI::Ptr<RHI::Device> device = RHI::RHISystemInterface::Get()->GetDevice();
+        m_windowContext = AZStd::make_shared<RPI::WindowContext>();
+        m_windowContext->Initialize(*device, m_nativeWindow->GetWindowHandle());
+
+        // Create the RPI::Scene, add some feature processors
+        RPI::SceneDescriptor sceneDesc;
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::CapsuleLightFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::DecalTextureArrayFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::DirectionalLightFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::DiskLightFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::ImageBasedLightFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::MeshFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::PointLightFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::PostProcessFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::QuadLightFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("ReflectionProbeFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::SkyBoxFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::SpotLightFeatureProcessor");
+        sceneDesc.m_featureProcessorNames.push_back("AZ::Render::TransformServiceFeatureProcessor");
+        m_scene = RPI::Scene::CreateScene(sceneDesc);
+
+        // Setup scene srg modification callback (to push per-frame values to the shaders)
+        RPI::ShaderResourceGroupCallback srgCallback = [this](RPI::ShaderResourceGroup* srg)
+        {
+            if (srg == nullptr)
+            {
+                return;
+            }
+            bool needCompile = false;
+            RHI::ShaderInputConstantIndex timeIndex = srg->FindShaderInputConstantIndex(Name{ "m_time" });
+            if (timeIndex.IsValid())
+            {
+                srg->SetConstant(timeIndex, aznumeric_cast<float>(m_simulateTime));
+                needCompile = true;
+            }
+            RHI::ShaderInputConstantIndex deltaTimeIndex = srg->FindShaderInputConstantIndex(Name{ "m_deltaTime" });
+            if (deltaTimeIndex.IsValid())
+            {
+                srg->SetConstant(deltaTimeIndex, m_deltaTime);
+                needCompile = true;
+            }
+
+            if (needCompile)
+            {
+                srg->Compile();
+            }
+        };
+        m_scene->SetShaderResourceGroupCallback(srgCallback);
+
+        // Link our RPI::Scene to the AzFramework::Scene
+        m_frameworkScene->SetSubsystem(m_scene.get());
+
+        // Create a custom pipeline descriptor
+        RPI::RenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.m_mainViewTagName = "MainCamera";       // Surface shaders render to the "MainCamera" tag
+        pipelineDesc.m_name = "SecondPipeline";              // Sets the debug name for this pipeline
+        pipelineDesc.m_rootPassTemplate = "MainPipeline";    // References a template in AtomSampleViewer\Passes\MainPipeline.pass
+        pipelineDesc.m_renderSettings.m_multisampleState.m_samples = 4;
+        m_pipeline = RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext);
+
+        m_scene->AddRenderPipeline(m_pipeline);
+        m_scene->Activate();
+        RPI::RPISystemInterface::Get()->RegisterScene(m_scene);
+
+        // Create a camera entity, hook it up to the RenderPipeline
+        m_cameraEntity = CreateEntity("WindowedSceneCamera", m_entityContext->GetContextId());
+        Debug::CameraComponentConfig cameraConfig(m_windowContext);
+        cameraConfig.m_fovY = Constants::HalfPi;
+        m_cameraEntity->CreateComponent(azrtti_typeid<Debug::CameraComponent>())->SetConfiguration(cameraConfig);
+        m_cameraEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+        m_cameraEntity->CreateComponent(azrtti_typeid<Debug::NoClipControllerComponent>());
+        m_cameraEntity->Init();
+        m_cameraEntity->Activate();
+        m_pipeline->SetDefaultViewFromEntity(m_cameraEntity->GetId());
+
+        // Create a Depth of Field entity
+        m_depthOfFieldEntity = CreateEntity("DepthOfField", m_entityContext->GetContextId());
+        m_depthOfFieldEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+
+        // Get the FeatureProcessors
+        m_meshFeatureProcessor = m_scene->GetFeatureProcessor<Render::MeshFeatureProcessorInterface>();
+        m_skyBoxFeatureProcessor = m_scene->GetFeatureProcessor<Render::SkyBoxFeatureProcessorInterface>();
+        m_pointLightFeatureProcessor = m_scene->GetFeatureProcessor<Render::PointLightFeatureProcessorInterface>();
+        m_spotLightFeatureProcessor = m_scene->GetFeatureProcessor<Render::SpotLightFeatureProcessorInterface>();
+        m_directionalLightFeatureProcessor = m_scene->GetFeatureProcessor<Render::DirectionalLightFeatureProcessorInterface>();
+        m_reflectionProbeFeatureProcessor = m_scene->GetFeatureProcessor<Render::ReflectionProbeFeatureProcessorInterface>();
+        m_postProcessFeatureProcessor = m_scene->GetFeatureProcessor<Render::PostProcessFeatureProcessorInterface>();
+
+        // Helper function to load meshes
+        const auto LoadMesh = [this](const char* modelPath) -> Render::MeshFeatureProcessorInterface::MeshHandle
+        {
+            AZ_Assert(m_meshFeatureProcessor, "Cannot find mesh feature processor on scene");
+            auto meshAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::ModelAsset>(modelPath, RPI::AssetUtils::TraceLevel::Assert);
+            Render::MeshFeatureProcessorInterface::MeshHandle meshHandle = m_meshFeatureProcessor->AcquireMesh(meshAsset);
+
+            return meshHandle;
+        };
+
+        // Create the ShaderBalls
+        {
+            for (uint32_t i = 0u; i < ShaderBallCount; i++)
+            {
+                m_shaderBallMeshHandles.push_back(LoadMesh("objects/shaderball_simple.azmodel"));
+                auto updateShaderBallTransform = [this, i](Data::Instance<RPI::Model> model)
+                {
+                    const Aabb& aabb = model->GetAabb();
+                    const Vector3 translation{ 0.0f, -aabb.GetMin().GetZ() * aznumeric_cast<float>(i), -aabb.GetMin().GetY() };
+                    const auto transform = Transform::CreateTranslation(translation);
+                    m_meshFeatureProcessor->SetTransform(m_shaderBallMeshHandles[i], transform);
+                };
+
+                // If the model is available already, set the tranform immediately, else utilize the EBus::Event feature
+                Data::Instance<RPI::Model> shaderBallModel = m_meshFeatureProcessor->GetModel(m_shaderBallMeshHandles[i]);
+                if (shaderBallModel)
+                {
+                    updateShaderBallTransform(shaderBallModel);
+                }
+                else
+                {
+                    m_shaderBallChangedHandles.push_back(ModelChangedHandler(updateShaderBallTransform));
+                    m_meshFeatureProcessor->ConnectModelChangeEventHandler(m_shaderBallMeshHandles[i], m_shaderBallChangedHandles.back());
+                }
+            }
+        }
+
+        // Create the floor
+        {
+            const Vector3 scale{ 24.f, 24.f, 1.0f };
+            const Vector3 translation{ 0.f, 0.f, 0.0f };
+            const auto transform = Transform::CreateTranslation(translation) * Transform::CreateScale(scale);
+            m_floorMeshHandle = LoadMesh("testdata/objects/cube/cube.azmodel");
+            m_meshFeatureProcessor->SetTransform(m_floorMeshHandle, transform);
+        }
+
+        // Create the Skybox
+        {
+            m_skyBoxFeatureProcessor->SetSkyboxMode(Render::SkyBoxMode::PhysicalSky);
+            m_skyBoxFeatureProcessor->Enable(true);
+        }
+
+        // Create PointLight
+        {
+            m_pointLightHandle = m_pointLightFeatureProcessor->AcquireLight();
+
+            const Vector3 pointLightPosition(4.0f, 0.0f, 5.0f);
+            m_pointLightFeatureProcessor->SetPosition(m_pointLightHandle, pointLightPosition);
+
+            Render::PhotometricValue rgbIntensity;
+            rgbIntensity.SetEffectiveSolidAngle(Render::PhotometricValue::OmnidirectionalSteradians);
+            m_pointLightFeatureProcessor->SetRgbIntensity(m_pointLightHandle, rgbIntensity.GetCombinedRgb<Render::PhotometricUnit::Candela>());
+
+            m_pointLightFeatureProcessor->SetBulbRadius(m_pointLightHandle, 4.0f);
+        }
+
+        // Create SpotLight
+        {
+            m_spotLightHandle = m_spotLightFeatureProcessor->AcquireLight();
+
+            const Vector3 spotLightPosition(3.0f, 0.0f, 4.0f);
+            m_spotLightFeatureProcessor->SetPosition(m_spotLightHandle, spotLightPosition);
+
+            Render::PhotometricValue rgbIntensity;
+            m_spotLightFeatureProcessor->SetRgbIntensity(m_spotLightHandle, Render::PhotometricColor<Render::PhotometricUnit::Candela>(Color::CreateOne() * 2000.0f));
+
+            const auto lightDir = Transform::CreateLookAt(
+                spotLightPosition,
+                Vector3::CreateZero());
+            m_spotLightFeatureProcessor->SetDirection(m_spotLightHandle, lightDir.GetBasis(1));
+
+            const float radius = sqrtf(2000.0f / 0.5f);
+            m_spotLightFeatureProcessor->SetAttenuationRadius(m_spotLightHandle, radius);
+            m_spotLightFeatureProcessor->SetShadowmapSize(m_spotLightHandle, Render::ShadowmapSize::Size512);
+            m_spotLightFeatureProcessor->SetConeAngles(m_spotLightHandle, 45.f, 55.f);
+            m_spotLightFeatureProcessor->SetShadowBoundaryWidthAngle(m_spotLightHandle, 0.25f);
+        }
+
+        // Create DirectionalLight
+        {
+            m_directionalLightHandle = m_directionalLightFeatureProcessor->AcquireLight();
+
+            // Get the camera configuration
+            {
+                Camera::Configuration config;
+                Camera::CameraRequestBus::EventResult(
+                    config,
+                    m_cameraEntity->GetId(),
+                    &Camera::CameraRequestBus::Events::GetCameraConfiguration);
+                m_directionalLightFeatureProcessor->SetCameraConfiguration(
+                    m_directionalLightHandle,
+                    config);
+            }
+
+            // Camera Transform
+            {
+                Transform transform = Transform::CreateIdentity();
+                TransformBus::EventResult(
+                    transform,
+                    m_cameraEntity->GetId(),
+                    &TransformBus::Events::GetWorldTM);
+                m_directionalLightFeatureProcessor->SetCameraTransform(
+                    m_directionalLightHandle, transform);
+            }
+
+            Render::PhotometricColor<Render::PhotometricUnit::Lux> lightColor(Color::CreateOne() * 50.0f);
+            m_directionalLightFeatureProcessor->SetRgbIntensity(m_directionalLightHandle, lightColor);
+
+            const Vector3 helperPosition(-3.0f, 0.0f, 4.0f);
+            const auto lightDir = Transform::CreateLookAt(
+                helperPosition,
+                Vector3::CreateZero());
+            m_directionalLightFeatureProcessor->SetDirection(m_directionalLightHandle, lightDir.GetBasis(1));
+
+            m_directionalLightFeatureProcessor->SetShadowmapSize(m_directionalLightHandle, Render::ShadowmapSize::Size512);
+            m_directionalLightFeatureProcessor->SetCascadeCount(m_directionalLightHandle, 2);
+        }
+
+        // Create ReflectionProbe
+        {
+            const Vector3 probePosition{ -5.0f, 0.0f, 1.5f };
+            const Transform probeTransform = Transform::CreateTranslation(probePosition);
+            m_reflectionProbeHandle = m_reflectionProbeFeatureProcessor->AddProbe(probeTransform, true);
+            m_reflectionProbeFeatureProcessor->ShowProbeVisualization(m_reflectionProbeHandle, true);
+        }
+
+        // Enable Depth of Field
+        {
+            // Setup the depth of field
+            auto* postProcessSettings = m_postProcessFeatureProcessor->GetOrCreateSettingsInterface(m_depthOfFieldEntity->GetId());
+            m_depthOfFieldSettings = postProcessSettings->GetOrCreateDepthOfFieldSettingsInterface();
+            m_depthOfFieldSettings->SetQualityLevel(1u);
+            m_depthOfFieldSettings->SetApertureF(0.5f);
+            m_depthOfFieldSettings->SetEnableDebugColoring(false);
+            m_depthOfFieldSettings->SetEnableAutoFocus(true);
+            m_depthOfFieldSettings->SetAutoFocusScreenPosition(Vector2{ 0.5f, 0.5f });
+            m_depthOfFieldSettings->SetAutoFocusSensitivity(1.0f);
+            m_depthOfFieldSettings->SetAutoFocusSpeed(Render::DepthOfField::AutoFocusSpeedMax);
+            m_depthOfFieldSettings->SetAutoFocusDelay(0.2f);
+            m_depthOfFieldSettings->SetCameraEntityId(m_cameraEntity->GetId());
+            m_depthOfFieldSettings->SetEnabled(true);
+            m_depthOfFieldSettings->OnConfigChanged();
+
+            m_depthOfFieldEntity->Init();
+            m_depthOfFieldEntity->Activate();
+        }
+
+        // IBL
+        m_defaultIbl.Init(m_scene.get());
+
+        TickBus::Handler::BusConnect();
+        AzFramework::WindowNotificationBus::Handler::BusConnect(m_nativeWindow->GetWindowHandle());
+    }
+
+    SecondWindowedScene::~SecondWindowedScene()
+    {
+        using namespace AZ;
+
+        // Disconnect hte busses
+        TickBus::Handler::BusDisconnect();
+        AzFramework::WindowNotificationBus::Handler::BusDisconnect(m_nativeWindow->GetWindowHandle());
+
+        m_defaultIbl.Reset();
+
+        // Release all the light types
+        m_pointLightFeatureProcessor->ReleaseLight(m_pointLightHandle);
+        m_spotLightFeatureProcessor->ReleaseLight(m_spotLightHandle);
+        m_directionalLightFeatureProcessor->ReleaseLight(m_directionalLightHandle);
+
+        // Release the probe
+        m_reflectionProbeFeatureProcessor->RemoveProbe(m_reflectionProbeHandle);
+        m_reflectionProbeHandle = nullptr;
+
+        // Release all meshes
+        for (auto& shaderBallMeshHandle : m_shaderBallMeshHandles)
+        {
+            m_meshFeatureProcessor->ReleaseMesh(shaderBallMeshHandle);
+        }
+        m_meshFeatureProcessor->ReleaseMesh(m_floorMeshHandle);
+
+        DestroyEntity(m_cameraEntity);
+        DestroyEntity(m_depthOfFieldEntity);
+
+        m_frameworkScene->UnsetSubsystem<RPI::Scene>();
+
+        // Remove the scene
+        m_scene->Deactivate();
+        m_scene->RemoveRenderPipeline(m_pipeline->GetId());
+        RPI::RPISystemInterface::Get()->UnregisterScene(m_scene);
+        bool sceneRemovedSuccessfully = false;
+        AzFramework::SceneSystemRequestBus::BroadcastResult(sceneRemovedSuccessfully, &AzFramework::SceneSystemRequests::RemoveScene, m_sceneName);
+        m_scene = nullptr;
+
+        m_windowContext->Shutdown();
+    }
+
+    void SecondWindowedScene::MoveCamera(bool enabled)
+    {
+        m_moveCamera = enabled;
+    }
+
+    // AZ::TickBus::Handler overrides ...
+    void SecondWindowedScene::OnTick(float deltaTime, AZ::ScriptTimePoint timePoint)
+    {
+        using namespace AZ;
+
+        m_deltaTime = deltaTime;
+        m_simulateTime = timePoint.GetSeconds();
+
+        // Move the camera a bit each frame
+        // Note: view space in this scene is right-handed, Z-up, Y-forward
+        const float dynamicOffsetScale = 4.0f;
+        if (m_moveCamera)
+        {
+            m_dynamicCameraOffset = Vector3(dynamicOffsetScale * sinf(aznumeric_cast<float>(timePoint.GetSeconds())), 0.0f, 0.0f);
+        }
+        Vector3 cameraPosition = m_cameraOffset + m_dynamicCameraOffset;
+        TransformBus::Event(m_cameraEntity->GetId(), &TransformBus::Events::SetLocalTranslation, cameraPosition);
+
+        const auto cameraDirection = Transform::CreateLookAt(
+            cameraPosition,
+            Vector3::CreateZero());
+        TransformBus::Event(m_cameraEntity->GetId(), &TransformBus::Events::SetLocalRotationQuaternion, cameraDirection.GetRotation());
+    }
+
+    void SecondWindowedScene::OnWindowClosed()
+    {
+        m_parent->OnChildWindowClosed();
+    }
+
+    AzFramework::NativeWindowHandle SecondWindowedScene::GetNativeWindowHandle()
+    {
+        if (m_nativeWindow)
+        {
+            return m_nativeWindow->GetWindowHandle();
+        }
+        else
+        {
+            return nullptr;
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////////
+    // MultiSceneExampleComponent
+
+    void MultiSceneExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MultiSceneExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }
+
+    MultiSceneExampleComponent::MultiSceneExampleComponent()
+    {
+    }
+
+    void MultiSceneExampleComponent::Activate()
+    {
+        using namespace AZ;
+
+        // Setup primary camera controls
+        Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<Debug::NoClipControllerComponent>());
+
+        RPI::ScenePtr scene = RPI::RPISystemInterface::Get()->GetDefaultScene();
+
+        // Setup Main Mesh Entity
+        {
+            auto bunnyAsset = RPI::AssetUtils::GetAssetByProductPath<RPI::ModelAsset>("objects/bunny.azmodel", RPI::AssetUtils::TraceLevel::Assert);
+            m_meshHandle = GetMeshFeatureProcessor()->AcquireMesh(bunnyAsset);
+            GetMeshFeatureProcessor()->SetTransform(m_meshHandle, Transform::CreateRotationZ(Constants::Pi));
+        }
+
+        // IBL
+        {
+            m_defaultIbl.Init(scene.get());
+        }
+
+        if (SupportsMultipleWindows())
+        {
+            OpenSecondSceneWindow();
+        }
+
+        TickBus::Handler::BusConnect();
+    }
+
+    void MultiSceneExampleComponent::Deactivate()
+    {
+        using namespace AZ;
+
+        TickBus::Handler::BusDisconnect();
+
+        Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &Debug::CameraControllerRequestBus::Events::Disable);
+
+        m_defaultIbl.Reset();
+
+        GetMeshFeatureProcessor()->ReleaseMesh(m_meshHandle);
+
+        if (m_windowedScene)
+        {
+            m_windowedScene = nullptr;
+        }
+    }
+
+    void MultiSceneExampleComponent::OnChildWindowClosed()
+    {
+        m_windowedScene = nullptr;
+    }
+
+    void MultiSceneExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        if (ImGui::Begin("Multi Scene Panel"))
+        {
+            if (m_windowedScene)
+            {
+                static bool moveCamera = false;
+                if (ImGui::Button("Close Second Scene Window"))
+                {
+                    m_windowedScene = nullptr;
+                    moveCamera = false;
+                }
+                if (ImGui::Checkbox("Move camera##MultiSceneExample", &moveCamera))
+                {
+                    if (m_windowedScene)
+                    {
+                        m_windowedScene->MoveCamera(moveCamera);
+                    }
+                }
+            }
+            else
+            {
+                if (ImGui::Button("Open Second Scene Window"))
+                {
+                    OpenSecondSceneWindow();
+                }
+            }
+        }
+        ImGui::End();
+    }
+
+    void MultiSceneExampleComponent::OpenSecondSceneWindow()
+    {
+        if (!m_windowedScene)
+        {
+            m_windowedScene = AZStd::make_unique<SecondWindowedScene>("SecondScene", this);
+        }
+    }
+
+
+} // namespace AtomSampleViewer

+ 145 - 0
Gem/Code/Source/MultiSceneExampleComponent.h

@@ -0,0 +1,145 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <Atom/RPI.Public/Base.h>
+#include <Atom/RPI.Public/WindowContext.h>
+
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <AzFramework/Windowing/WindowBus.h>
+#include <AzFramework/Windowing/NativeWindow.h>
+
+#include <Atom/Feature/CoreLights/PointLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/SpotLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessorInterface.h>
+#include <Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessor.h>
+
+#include <Utils/Utils.h>
+
+struct ImGuiContext;
+
+namespace AtomSampleViewer
+{
+    class SecondWindowedScene 
+        : public AZ::TickBus::Handler
+        , public AzFramework::WindowNotificationBus::Handler
+    {
+        using ModelChangedHandler = AZ::Render::MeshFeatureProcessorInterface::ModelChangedEvent::Handler;
+        using PointLightHandle = AZ::Render::PointLightFeatureProcessorInterface::LightHandle;
+        using SpotLightHandle = AZ::Render::SpotLightFeatureProcessorInterface::LightHandle;
+        using DirectionalLightHandle = AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle;
+        using ReflectinoProbeHandle = AZ::Render::ReflectionProbeHandle;
+
+        static constexpr uint32_t ShaderBallCount = 12u;
+
+    public:
+        SecondWindowedScene(AZStd::string_view sceneName, class MultiSceneExampleComponent* parent);
+        ~SecondWindowedScene();
+
+        AzFramework::NativeWindowHandle GetNativeWindowHandle();
+
+        void MoveCamera(bool enabled);
+
+        // AZ::TickBus::Handler overrides ...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        // AzFramework::WindowNotificationBus::Handler overrides ...
+        void OnWindowClosed() override;
+
+    private:
+        AZStd::unique_ptr<AzFramework::NativeWindow> m_nativeWindow;
+        AZStd::shared_ptr<AZ::RPI::WindowContext> m_windowContext;
+        AZStd::unique_ptr<AzFramework::EntityContext> m_entityContext;
+        AZStd::string m_sceneName;
+        AzFramework::Scene* m_frameworkScene = nullptr;
+        AZ::RPI::ScenePtr m_scene;
+        AZ::RPI::RenderPipelinePtr m_pipeline;
+
+        // Entities
+        AZ::Entity* m_cameraEntity = nullptr;
+        AZ::Entity* m_depthOfFieldEntity = nullptr;
+
+        // Feature Settings
+        AZ::Render::DepthOfFieldSettingsInterface* m_depthOfFieldSettings = nullptr;
+
+        // FeatureProcessors
+        AZ::Render::MeshFeatureProcessorInterface* m_meshFeatureProcessor = nullptr;
+        AZ::Render::SkyBoxFeatureProcessorInterface* m_skyBoxFeatureProcessor = nullptr;
+        AZ::Render::PointLightFeatureProcessorInterface* m_pointLightFeatureProcessor = nullptr;
+        AZ::Render::SpotLightFeatureProcessorInterface* m_spotLightFeatureProcessor = nullptr;
+        AZ::Render::DirectionalLightFeatureProcessorInterface* m_directionalLightFeatureProcessor = nullptr;
+        AZ::Render::PostProcessFeatureProcessorInterface* m_postProcessFeatureProcessor = nullptr;
+        AZ::Render::ReflectionProbeFeatureProcessorInterface* m_reflectionProbeFeatureProcessor = nullptr;
+
+        // Meshes
+        AZStd::vector<AZ::Render::MeshFeatureProcessorInterface::MeshHandle> m_shaderBallMeshHandles;
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_floorMeshHandle;
+
+        // Model change handlers
+        AZStd::vector<ModelChangedHandler> m_shaderBallChangedHandles;
+
+        // Various FeatureProcessor handles
+        PointLightHandle m_pointLightHandle;
+        SpotLightHandle m_spotLightHandle;
+        DirectionalLightHandle m_directionalLightHandle;
+        ReflectinoProbeHandle m_reflectionProbeHandle;
+
+        double m_simulateTime = 0.0f;
+        float m_deltaTime = 0.0f;
+        MultiSceneExampleComponent* m_parent = nullptr;
+        const AZ::Vector3 m_cameraOffset{ 0.0f, -4.0f, 2.0f };
+        AZ::Vector3 m_dynamicCameraOffset{ 3.73f, 0.0f, 0.0f };
+        bool m_moveCamera = false;
+        Utils::DefaultIBL m_defaultIbl;
+    };
+    
+    //! A sample component to demonstrate multiple scenes.
+    class MultiSceneExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(MultiSceneExampleComponent, "{FB0F55AE-6708-47BE-87EB-DD1EB3EF5CD1}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        MultiSceneExampleComponent();
+        ~MultiSceneExampleComponent() override = default;
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+        void OnChildWindowClosed();
+
+    private:        
+        // AZ::TickBus::Handler overrides ...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        void OpenSecondSceneWindow();
+
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_meshHandle;
+        AZ::Component* m_mainCameraControlComponent = nullptr;
+
+        AZStd::unique_ptr<SecondWindowedScene> m_windowedScene;
+
+        // Lights
+        Utils::DefaultIBL m_defaultIbl;
+    };
+
+} // namespace AtomSampleViewer

+ 269 - 0
Gem/Code/Source/MultiViewSingleSceneAuxGeomExampleComponent.cpp

@@ -0,0 +1,269 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MultiViewSingleSceneAuxGeomExampleComponent.h>
+#include <AuxGeomSharedDrawFunctions.h>
+
+#include <Atom/Component/DebugCamera/CameraComponent.h>
+#include <Atom/Component/DebugCamera/NoClipControllerComponent.h>
+
+#include <Atom/Feature/PostProcessing/PostProcessingConstants.h>
+
+#include <Atom/RPI.Public/RenderPipeline.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RHI/RHISystemInterface.h>
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/AuxGeom/AuxGeomDraw.h>
+#include <Atom/RPI.Public/AuxGeom/AuxGeomFeatureProcessorInterface.h>
+
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+
+#include <AzCore/Math/MatrixUtils.h>
+
+#include <AzCore/Component/Entity.h>
+
+#include <AzFramework/Components/TransformComponent.h>
+#include <AzFramework/Scene/SceneSystemBus.h>
+
+#include <SampleComponentConfig.h>
+#include <SampleComponentManager.h>
+#include <EntityUtilityFunctions.h>
+
+#include <RHI/BasicRHIComponent.h>
+#include <AtomSampleViewerOptions.h>
+
+namespace AtomSampleViewer
+{
+    //////////////////////////////////////////////////////////////////////////
+    // WindowedView
+
+    //! A simple example for how to set up a second window, view, renderPipeline, and basic entities.
+    class WindowedView final
+        : public AzFramework::WindowNotificationBus::Handler
+    {
+        friend MultiViewSingleSceneAuxGeomExampleComponent;
+    protected:
+        AZStd::unique_ptr<AzFramework::NativeWindow> m_nativeWindow;
+        AZStd::shared_ptr<AZ::RPI::WindowContext> m_windowContext;
+        AZ::RPI::RenderPipelinePtr m_pipeline;
+        AZ::Entity* m_cameraEntity = nullptr;
+        AZ::RPI::ViewPtr m_view;
+        double m_simulateTime = 0.0f;
+        float m_deltaTime = 0.0f;
+        MultiViewSingleSceneAuxGeomExampleComponent* m_parent;
+
+    public:
+        WindowedView(AzFramework::EntityContextId contextId, MultiViewSingleSceneAuxGeomExampleComponent *parent)
+        : m_parent(parent)
+        {
+            m_parent = parent;
+
+            // Create a NativeWindow and WindowContext
+            m_nativeWindow = AZStd::make_unique<AzFramework::NativeWindow>("Multi View Single Scene: Second Window", AzFramework::WindowGeometry(0, 0, 640, 480));
+            m_nativeWindow->Activate();
+            AZ::RHI::Ptr<AZ::RHI::Device> device = AZ::RHI::RHISystemInterface::Get()->GetDevice();
+            m_windowContext = AZStd::make_shared<AZ::RPI::WindowContext>();
+            m_windowContext->Initialize(*device, m_nativeWindow->GetWindowHandle());
+
+            auto scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+
+            // Create a custom pipeline descriptor
+            AZ::RPI::RenderPipelineDescriptor pipelineDesc;
+            pipelineDesc.m_mainViewTagName = "MainCamera";          //Surface shaders render to the "MainCamera" tag
+            pipelineDesc.m_name = "SecondPipeline";                 //Sets the debug name for this pipeline
+            pipelineDesc.m_rootPassTemplate = "MainPipeline";    //References a template in AtomSampleViewer\Passes\PassTemplates.azasset
+            pipelineDesc.m_renderSettings.m_multisampleState.m_samples = 4;
+            m_pipeline = AZ::RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext);
+
+            scene->AddRenderPipeline(m_pipeline);
+
+            // Create a camera entity, hook it up to the RenderPipeline
+            m_cameraEntity = CreateEntity("WindowedViewCamera", contextId);
+            AZ::Debug::CameraComponentConfig cameraConfig(m_windowContext);
+            cameraConfig.m_fovY = AZ::Constants::QuarterPi;
+            AZ::Debug::CameraComponent* camComponent = static_cast<AZ::Debug::CameraComponent*>(m_cameraEntity->CreateComponent(azrtti_typeid<AZ::Debug::CameraComponent>()));
+            camComponent->SetConfiguration(cameraConfig);
+            m_cameraEntity->CreateComponent(azrtti_typeid<AzFramework::TransformComponent>());
+            m_cameraEntity->CreateComponent(azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+            m_cameraEntity->Activate();
+            m_pipeline->SetDefaultViewFromEntity(m_cameraEntity->GetId());
+            m_view = camComponent->GetView();
+
+            AzFramework::WindowNotificationBus::Handler::BusConnect(m_nativeWindow->GetWindowHandle());
+        }
+
+        ~WindowedView()
+        {
+            AzFramework::WindowNotificationBus::Handler::BusDisconnect(m_nativeWindow->GetWindowHandle());
+
+            DestroyEntity(m_cameraEntity);
+
+            m_pipeline->RemoveFromScene();
+            m_pipeline = nullptr;
+
+            m_windowContext->Shutdown();
+            m_windowContext = nullptr;
+        }
+
+        // AzFramework::WindowNotificationBus::Handler overrides ...
+        void OnWindowClosed() override
+        {
+            m_parent->OnChildWindowClosed();
+        }
+
+        AzFramework::NativeWindowHandle GetNativeWindowHandle()
+        {
+            if (m_nativeWindow)
+            {
+                return m_nativeWindow->GetWindowHandle();
+            }
+            else
+            {
+                return nullptr;
+            }
+        }
+    };
+
+    //////////////////////////////////////////////////////////////////////////
+    // MultiViewSingleSceneAuxGeomExampleComponent
+
+    void MultiViewSingleSceneAuxGeomExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<MultiViewSingleSceneAuxGeomExampleComponent, AZ::Component>()
+                ->Version(0)
+                ;
+        }
+    }    
+
+    MultiViewSingleSceneAuxGeomExampleComponent::MultiViewSingleSceneAuxGeomExampleComponent()
+    {
+    }
+
+    void MultiViewSingleSceneAuxGeomExampleComponent::Activate()
+    {
+        // Setup primary camera controls
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::NoClipControllerComponent>());
+
+        OpenSecondSceneWindow();
+
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void MultiViewSingleSceneAuxGeomExampleComponent::Deactivate()
+    {
+        AZ::TickBus::Handler::BusDisconnect();
+
+        AZ::Debug::CameraControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+
+        if(m_windowedView)
+        {
+            m_windowedView = nullptr;
+        }
+    }
+
+    void MultiViewSingleSceneAuxGeomExampleComponent::OnChildWindowClosed()
+    {
+        m_windowedView = nullptr;
+    }
+
+    void MultiViewSingleSceneAuxGeomExampleComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint timePoint)
+    {
+        DrawAuxGeom();
+
+        if (SupportsMultipleWindows() && ImGui::Begin("Multi View Panel"))
+        {
+            if(m_windowedView)
+            {
+                if (ImGui::Button("Close Second View Window"))
+                {
+                    m_windowedView = nullptr;
+                }
+            }
+            else
+            {
+                if (ImGui::Button("Open Second View Window"))
+                {
+                    OpenSecondSceneWindow();
+                }
+            }
+            ImGui::End();
+        }
+
+        if (m_windowedView)
+        {
+            // duplicate first camera changes to the 2nd camera
+            AZ::Transform mainCameraTransform;
+            AZ::TransformBus::EventResult(mainCameraTransform, GetCameraEntityId(), &AZ::TransformBus::Events::GetWorldTM);
+            Camera::CameraComponentRequests* cameraInterface = Camera::CameraRequestBus::FindFirstHandler(GetCameraEntityId());
+            float fovRadians = cameraInterface->GetFovRadians();
+            float nearClipDistance = cameraInterface->GetNearClipDistance();
+            float farClipDistance = cameraInterface->GetFarClipDistance();
+
+            AZ::EntityId secondCameraEntityId = m_windowedView->m_cameraEntity->GetId();
+            AZ::TransformBus::Event(secondCameraEntityId, &AZ::TransformBus::Events::SetWorldTM, mainCameraTransform);
+            Camera::CameraComponentRequests* secondCamInterface = Camera::CameraRequestBus::FindFirstHandler(secondCameraEntityId);
+            secondCamInterface->SetFovRadians(fovRadians);
+            secondCamInterface->SetNearClipDistance(nearClipDistance);
+            secondCamInterface->SetFarClipDistance(farClipDistance);
+        }
+    }
+
+    void MultiViewSingleSceneAuxGeomExampleComponent::OpenSecondSceneWindow()
+    {
+        if (SupportsMultipleWindows() && !m_windowedView)
+        {
+            m_windowedView = AZStd::make_unique<WindowedView>(GetEntityContextId(), this);
+        }
+    }
+
+    void MultiViewSingleSceneAuxGeomExampleComponent::DrawAuxGeom() const
+    {
+        auto scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene();
+        auto auxGeomFP = scene->GetFeatureProcessor<AZ::RPI::AuxGeomFeatureProcessorInterface>();
+        if (auto auxGeom = auxGeomFP->GetDrawQueue())
+        {
+            DrawBackgroundBox(auxGeom);
+
+            DrawThreeGridsOfPoints(auxGeom);
+
+            DrawAxisLines(auxGeom);
+
+            DrawLines(auxGeom);
+
+            DrawBoxes(auxGeom, -20.0f);
+
+            Draw2DWireRect(auxGeom, AZ::Colors::Red, 1.0f);
+        }
+
+        if (m_windowedView)
+        {
+            if (auto auxGeom = auxGeomFP->GetDrawQueueForView(m_windowedView->m_view.get()))
+            {
+                DrawTriangles(auxGeom);
+
+                DrawShapes(auxGeom);
+
+                DrawBoxes(auxGeom, 10.0f);
+
+                DrawDepthTestPrimitives(auxGeom);
+
+                Draw2DWireRect(auxGeom, AZ::Colors::Yellow, 0.9f);
+            }
+        }
+    }
+
+    
+
+} // namespace AtomSampleViewer

+ 63 - 0
Gem/Code/Source/MultiViewSingleSceneAuxGeomExampleComponent.h

@@ -0,0 +1,63 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#include <CommonSampleComponentBase.h>
+
+#include <Atom/RPI.Public/Base.h>
+#include <Atom/RPI.Public/WindowContext.h>
+
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <AzFramework/Windowing/WindowBus.h>
+#include <AzFramework/Windowing/NativeWindow.h>
+
+struct ImGuiContext;
+
+namespace AtomSampleViewer
+{
+    
+    //! A sample component to demonstrate multiple scenes.
+    class MultiViewSingleSceneAuxGeomExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(MultiViewSingleSceneAuxGeomExampleComponent, "{B5B97744-407C-467B-AE21-23323454F988}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        MultiViewSingleSceneAuxGeomExampleComponent();
+        ~MultiViewSingleSceneAuxGeomExampleComponent() override = default;
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+        void OnChildWindowClosed();
+
+    private:        
+        // AZ::TickBus::Handler overrides ...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint timePoint) override;
+
+        void OpenSecondSceneWindow();
+
+        void DrawAuxGeom() const;
+
+        AZ::Component* m_mainCameraControlComponent = nullptr;
+
+        AZStd::unique_ptr<class WindowedView> m_windowedView;
+    };
+
+} // namespace AtomSampleViewer

+ 455 - 0
Gem/Code/Source/ParallaxMappingExampleComponent.cpp

@@ -0,0 +1,455 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <ParallaxMappingExampleComponent.h>
+
+#include <Atom/Component/DebugCamera/ArcBallControllerComponent.h>
+
+#include <Atom/RPI.Public/RPISystemInterface.h>
+#include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+
+#include <Automation/ScriptableImGui.h>
+#include <AzCore/Component/TransformBus.h>
+
+#include <RHI/BasicRHIComponent.h>
+
+namespace AtomSampleViewer
+{
+    static const char* ParallaxEnableName = "parallax.enable";
+    static const char* PdoEnableName = "parallax.pdo";
+    static const char* ParallaxFactorName = "parallax.factor";
+    static const char* ParallaxAlgorithmName = "parallax.algorithm";
+    static const char* ParallaxQualityName = "parallax.quality";
+    static const char* ParallaxUvIndexName = "parallax.textureMapUv";
+
+    static const char* AmbientOcclusionUvIndexName = "ambientOcclusion.textureMapUv";
+    static const char* BaseColorUvIndexName = "baseColor.textureMapUv";
+    static const char* NormalUvIndexName = "normal.textureMapUv";
+    static const char* RoughnessUvIndexName = "roughness.textureMapUv";
+
+    static const char* CenterUVName = "uv.center";
+    static const char* TileUName = "uv.tileU";
+    static const char* TileVName = "uv.tileV";
+    static const char* OffsetUName = "uv.offsetU";
+    static const char* OffsetVName = "uv.offsetV";
+    static const char* RotationUVName = "uv.rotateDegrees";
+    static const char* ScaleUVName = "uv.scale";
+
+    // Must align with enum value in StandardPbr.materialtype
+    static const char* ParallaxAlgorithmList[] =
+    {
+        "Basic", "Steep", "POM", "Relief", "ContactRefinement"
+    };
+    static const char* ParallaxQualityList[] =
+    {
+        "Low", "Medium", "High", "Ultra"
+    };
+    static const char* ParallaxUvSetList[] =
+    {
+        "UV0", "UV1"
+    };
+
+    void ParallaxMappingExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class < ParallaxMappingExampleComponent, AZ::Component>()->Version(0);
+        }
+    }
+
+    ParallaxMappingExampleComponent::ParallaxMappingExampleComponent()
+        : m_imguiSidebar("@user@/ParallaxMappingExampleComponent/sidebar.xml")
+    {
+    }
+
+    void ParallaxMappingExampleComponent::Activate()
+    {
+        // Asset
+        m_planeAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>("objects/plane.azmodel", AZ::RPI::AssetUtils::TraceLevel::Assert);
+        m_boxAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>("objects/cube.azmodel", AZ::RPI::AssetUtils::TraceLevel::Assert);
+        m_parallaxMaterialAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::MaterialAsset>("testdata/materials/parallaxrock.azmaterial", AZ::RPI::AssetUtils::TraceLevel::Assert);
+        m_defaultMaterialAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::MaterialAsset>("materials/defaultpbr.azmaterial", AZ::RPI::AssetUtils::TraceLevel::Assert);
+        m_parallaxMaterial = AZ::RPI::Material::Create(m_parallaxMaterialAsset);
+        m_defaultMaterial = AZ::RPI::Material::Create(m_defaultMaterialAsset);
+
+        m_planeTransform = AZ::Transform::CreateScale(AZ::Vector3(5, 5, 5));
+        m_planeHandle = LoadMesh(m_planeAsset, m_parallaxMaterial, m_planeTransform);
+        m_boxHandle = LoadMesh(m_boxAsset, m_defaultMaterial, AZ::Transform::CreateIdentity());
+
+        // Material index
+        m_parallaxEnableIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(ParallaxEnableName));
+        m_parallaxFactorIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(ParallaxFactorName));
+        m_parallaxAlgorithmIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(ParallaxAlgorithmName));
+        m_parallaxQualityIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(ParallaxQualityName));
+        m_parallaxUvIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(ParallaxUvIndexName));
+        m_pdoEnableIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(PdoEnableName));
+
+        m_ambientOcclusionUvIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(AmbientOcclusionUvIndexName));
+        m_baseColorUvIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(BaseColorUvIndexName));
+        m_normalUvIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(NormalUvIndexName));
+        m_roughnessUvIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(RoughnessUvIndexName));
+
+        m_centerUVIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(CenterUVName));
+        m_tileUIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(TileUName));
+        m_tileVIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(TileVName));
+        m_offsetUIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(OffsetUName));
+        m_offsetVIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(OffsetVName));
+        m_rotationUVIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(RotationUVName));
+        m_scaleUVIndex = m_parallaxMaterial->FindPropertyIndex(AZ::Name(ScaleUVName));
+
+        SaveCameraConfiguration();
+        // Camera
+        AZ::Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &AZ::Debug::CameraControllerRequestBus::Events::Enable,
+            azrtti_typeid<AZ::Debug::ArcBallControllerComponent>());
+
+        ConfigureCameraToLookDown();
+        SetCameraConfiguration();
+
+        // Light
+        AZ::RPI::Scene* scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get();
+        m_directionalLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::DirectionalLightFeatureProcessorInterface>();
+        CreateDirectionalLight();
+        m_spotLightFeatureProcessor = scene->GetFeatureProcessor<AZ::Render::SpotLightFeatureProcessorInterface>();
+        CreateSpotLight();
+
+        m_imguiSidebar.Activate();
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void ParallaxMappingExampleComponent::Deactivate()
+    {
+        GetMeshFeatureProcessor()->ReleaseMesh(m_planeHandle);
+        GetMeshFeatureProcessor()->ReleaseMesh(m_boxHandle);
+
+        RestoreCameraConfiguration();
+        AZ::Debug::CameraControllerRequestBus::Event(
+            GetCameraEntityId(),
+            &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+
+        m_directionalLightFeatureProcessor->ReleaseLight(m_directionalLightHandle);
+        m_spotLightFeatureProcessor->ReleaseLight(m_spotLightHandle);
+
+        m_imguiSidebar.Deactivate();
+        AZ::TickBus::Handler::BusDisconnect();
+    }
+
+    void ParallaxMappingExampleComponent::ConfigureCameraToLookDown()
+    {
+        const float CameraDistance = 5.0f;
+        const float CameraPitch = -0.8f;
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetPitch, CameraPitch);
+        AZ::Debug::ArcBallControllerRequestBus::Event(GetCameraEntityId(), &AZ::Debug::ArcBallControllerRequestBus::Events::SetDistance, CameraDistance);
+    }
+
+    AZ::Render::MeshFeatureProcessorInterface::MeshHandle ParallaxMappingExampleComponent::LoadMesh(
+        AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset,
+        AZ::Data::Instance<AZ::RPI::Material> material,
+        AZ::Transform transform)
+    {
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle meshHandle = GetMeshFeatureProcessor()->AcquireMesh(modelAsset, material);
+        GetMeshFeatureProcessor()->SetTransform(meshHandle, transform);
+        return meshHandle;
+    }
+
+    void ParallaxMappingExampleComponent::CreateDirectionalLight()
+    {
+        const AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle handle = m_directionalLightFeatureProcessor->AcquireLight();
+
+        m_directionalLightFeatureProcessor->SetShadowmapSize(handle, AZ::Render::ShadowmapSize::Size2048);
+        m_directionalLightFeatureProcessor->SetCascadeCount(handle, 4);
+        m_directionalLightFeatureProcessor->SetShadowmapFrustumSplitSchemeRatio(handle, 0.5f);
+        m_directionalLightFeatureProcessor->SetViewFrustumCorrectionEnabled(handle, true);
+        m_directionalLightFeatureProcessor->SetShadowFilterMethod(handle, AZ::Render::ShadowFilterMethod::Esm);
+        m_directionalLightFeatureProcessor->SetShadowBoundaryWidth(handle, 0.03f);
+        m_directionalLightFeatureProcessor->SetPredictionSampleCount(handle, 8);
+        m_directionalLightFeatureProcessor->SetFilteringSampleCount(handle, 32);
+        m_directionalLightFeatureProcessor->SetGroundHeight(handle, 0.f);
+
+        m_directionalLightHandle = handle;
+    }
+
+    void ParallaxMappingExampleComponent::CreateSpotLight()
+    {
+        AZ::Render::SpotLightFeatureProcessorInterface* const featureProcessor = m_spotLightFeatureProcessor;
+        const AZ::Render::SpotLightFeatureProcessorInterface::LightHandle handle = featureProcessor->AcquireLight();
+
+        featureProcessor->SetAttenuationRadius( handle, sqrtf(500.f / CutoffIntensity));
+        featureProcessor->SetConeAngles( handle, 45.f * ConeAngleInnerRatio, 45.f);
+        featureProcessor->SetShadowmapSize( handle, AZ::Render::ShadowmapSize::Size2048);
+
+        m_spotLightHandle = handle;
+        
+    }
+
+    void ParallaxMappingExampleComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
+    {
+        if (m_lightAutoRotate)
+        {
+            m_lightRotationAngle = fmodf(m_lightRotationAngle + deltaTime, AZ::Constants::TwoPi);
+        }
+
+        const auto location = AZ::Vector3(
+            5 * sinf(m_lightRotationAngle),
+            5 * cosf(m_lightRotationAngle),
+            5);
+        auto transform = AZ::Transform::CreateLookAt(
+            location,
+            AZ::Vector3::CreateZero());
+
+        if (m_lightType)
+        {
+            AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Lux> directionalLightColor(AZ::Color::CreateZero());
+            AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Candela> spotLightColor(AZ::Color::CreateOne() * 500.f);
+            m_directionalLightFeatureProcessor->SetRgbIntensity(m_directionalLightHandle, directionalLightColor);
+            m_spotLightFeatureProcessor->SetRgbIntensity(m_spotLightHandle, spotLightColor);
+        }
+        else
+        {
+            AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Lux> directionalLightColor(AZ::Color::CreateOne() * 5.f);
+            AZ::Render::PhotometricColor<AZ::Render::PhotometricUnit::Candela> spotLightColor(AZ::Color::CreateZero());
+            m_directionalLightFeatureProcessor->SetRgbIntensity(m_directionalLightHandle, directionalLightColor);
+            m_spotLightFeatureProcessor->SetRgbIntensity(m_spotLightHandle, spotLightColor);
+        }
+
+        m_spotLightFeatureProcessor->SetPosition(m_spotLightHandle, location);
+        m_spotLightFeatureProcessor->SetDirection(m_spotLightHandle, transform.GetBasis(1));
+        m_directionalLightFeatureProcessor->SetDirection(m_directionalLightHandle, transform.GetBasis(1));
+
+        // Camera Configuration
+        {
+            Camera::Configuration config;
+            Camera::CameraRequestBus::EventResult(
+                config,
+                GetCameraEntityId(),
+                &Camera::CameraRequestBus::Events::GetCameraConfiguration);
+            m_directionalLightFeatureProcessor->SetCameraConfiguration(
+                m_directionalLightHandle,
+                config);
+        }
+
+        // Camera Transform
+        {
+            transform = AZ::Transform::CreateIdentity();
+            AZ::TransformBus::EventResult(
+                transform,
+                GetCameraEntityId(),
+                &AZ::TransformBus::Events::GetWorldTM);
+            m_directionalLightFeatureProcessor->SetCameraTransform(
+                m_directionalLightHandle, transform);
+        }
+
+        // Plane Transform
+        {
+            m_planeTransform.SetRotation(AZ::Quaternion::CreateRotationZ(m_planeRotationAngle));
+            GetMeshFeatureProcessor()->SetTransform(m_planeHandle, m_planeTransform);
+        }
+
+        DrawSidebar();
+    }
+
+    void ParallaxMappingExampleComponent::SetCameraConfiguration()
+    {
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFarClipDistance,
+            20.f);
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFovRadians,
+            AZ::Constants::QuarterPi);
+    }
+
+    void ParallaxMappingExampleComponent::SaveCameraConfiguration()
+    {
+        Camera::CameraRequestBus::EventResult(
+            m_originalFarClipDistance,
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::GetFarClipDistance);
+        Camera::CameraRequestBus::EventResult(
+            m_originalCameraFovRadians,
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::GetFovRadians);
+    }
+    void ParallaxMappingExampleComponent::RestoreCameraConfiguration()
+    {
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFarClipDistance,
+            m_originalFarClipDistance);
+        Camera::CameraRequestBus::Event(
+            GetCameraEntityId(),
+            &Camera::CameraRequestBus::Events::SetFovRadians,
+            m_originalCameraFovRadians);
+    }
+
+    void ParallaxMappingExampleComponent::DrawSidebar()
+    {
+        if (m_imguiSidebar.Begin())
+        {
+            bool parallaxSettingChanged = false;
+            bool planeUVChanged = false;
+
+            ImGui::Spacing();
+            {
+                ScriptableImGui::ScopedNameContext context{ "Lighting" };
+                ImGui::Text("Lighting");
+                ImGui::Indent();
+                {
+                    ScriptableImGui::RadioButton("Directional Light", &m_lightType, 0);
+                    ScriptableImGui::RadioButton("Spot Light", &m_lightType, 1);
+                    ScriptableImGui::Checkbox("Auto Rotation", &m_lightAutoRotate);
+                    ScriptableImGui::SliderAngle("Direction", &m_lightRotationAngle, 0, 360);
+                }
+                ImGui::Unindent();
+            }
+            
+            ImGui::Separator();
+
+            {
+                ScriptableImGui::ScopedNameContext context{ "Parallax Setting" };
+                ImGui::Text("Parallax Setting");
+                ImGui::Indent();
+                {
+                    bool parallaxEnableChanged = false;
+                    bool pdoEnableChanged = false;
+                    bool factorChanged = false;
+                    bool algorithmChanged = false;
+                    bool qualityChanged = false;
+                    bool uvChanged = false;
+
+                    parallaxEnableChanged = ScriptableImGui::Checkbox("Enable Parallax", &m_parallaxEnable);
+                    if (parallaxEnableChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_parallaxEnableIndex, m_parallaxEnable);
+                    }
+
+                    if (m_parallaxEnable)
+                    {
+                        pdoEnableChanged = ScriptableImGui::Checkbox("Enable Pdo", &m_pdoEnable);
+                        if (pdoEnableChanged)
+                        {
+                            m_parallaxMaterial->SetPropertyValue(m_pdoEnableIndex, m_pdoEnable);
+                        }
+
+                        factorChanged = ScriptableImGui::SliderFloat("Factor", &m_parallaxFactor, 0.0f, 0.1f);
+                        if (factorChanged)
+                        {
+                            m_parallaxMaterial->SetPropertyValue(m_parallaxFactorIndex, m_parallaxFactor);
+                        }
+
+                        algorithmChanged = ScriptableImGui::Combo("Algorithm", &m_parallaxAlgorithm, ParallaxAlgorithmList, AZ_ARRAY_SIZE(ParallaxAlgorithmList));
+                        if (algorithmChanged)
+                        {
+                            m_parallaxMaterial->SetPropertyValue(m_parallaxAlgorithmIndex, static_cast<uint32_t>(m_parallaxAlgorithm));
+                        }
+
+                        qualityChanged = ScriptableImGui::Combo("Quality", &m_parallaxQuality, ParallaxQualityList, AZ_ARRAY_SIZE(ParallaxQualityList));
+                        if (qualityChanged)
+                        {
+                            m_parallaxMaterial->SetPropertyValue(m_parallaxQualityIndex, static_cast<uint32_t>(m_parallaxQuality));
+                        }
+
+                        uvChanged = ScriptableImGui::Combo("UV", &m_parallaxUv, ParallaxUvSetList, AZ_ARRAY_SIZE(ParallaxUvSetList));
+                        if (uvChanged)
+                        {
+                            m_parallaxMaterial->SetPropertyValue(m_parallaxUvIndex, static_cast<uint32_t>(m_parallaxUv));
+                            m_parallaxMaterial->SetPropertyValue(m_ambientOcclusionUvIndex, static_cast<uint32_t>(m_parallaxUv));
+                            m_parallaxMaterial->SetPropertyValue(m_baseColorUvIndex, static_cast<uint32_t>(m_parallaxUv));
+                            m_parallaxMaterial->SetPropertyValue(m_normalUvIndex, static_cast<uint32_t>(m_parallaxUv));
+                            m_parallaxMaterial->SetPropertyValue(m_roughnessUvIndex, static_cast<uint32_t>(m_parallaxUv));
+                        }
+                    }
+
+                    parallaxSettingChanged = parallaxEnableChanged || pdoEnableChanged || factorChanged || algorithmChanged || qualityChanged || uvChanged;
+                }
+                ImGui::Unindent();
+            }
+
+            ImGui::Separator();
+
+            {
+                ScriptableImGui::ScopedNameContext context{ "Plane Setting" };
+                ImGui::Text("Plane Setting");
+                ImGui::Indent();
+                {
+                    ScriptableImGui::SliderAngle("Rotation", &m_planeRotationAngle, 0, 360);
+
+                    bool centerUChanged = false;
+                    bool centerVChanged = false;
+                    bool tileUChanged = false;
+                    bool tileVChanged = false;
+                    bool offsetUChanged = false;
+                    bool offsetVChanged = false;
+                    bool rotationUVChanged = false;
+                    bool scaleChanged = false;
+
+                    centerUChanged = ScriptableImGui::SliderFloat("Center U", &m_planeCenterU, -1.f, 1.f);
+                    centerVChanged = ScriptableImGui::SliderFloat("Center V", &m_planeCenterV, -1.f, 1.f);
+                    if (centerUChanged || centerVChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_centerUVIndex, AZ::Vector2(m_planeCenterU, m_planeCenterV));
+                    }
+                    
+                    tileUChanged = ScriptableImGui::SliderFloat("Tile U", &m_planeTileU, 0.f, 2.f);
+                    if (tileUChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_tileUIndex, m_planeTileU);
+                    }
+
+                    tileVChanged = ScriptableImGui::SliderFloat("Tile V", &m_planeTileV, 0.f, 2.f);
+                    if (tileVChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_tileVIndex, m_planeTileV);
+                    }
+
+                    offsetUChanged = ScriptableImGui::SliderFloat("Offset U", &m_planeOffsetU, -1.f, 1.f);
+                    if (offsetUChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_offsetUIndex, m_planeOffsetU);
+                    }
+
+                    offsetVChanged = ScriptableImGui::SliderFloat("Offset V", &m_planeOffsetV, -1.f, 1.f);
+                    if (offsetVChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_offsetVIndex, m_planeOffsetV);
+                    }
+
+                    rotationUVChanged = ScriptableImGui::SliderFloat("Rotation UV", &m_planeRotateUV, -180.f, 180.f);
+                    if (rotationUVChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_rotationUVIndex, m_planeRotateUV);
+                    }
+
+                    scaleChanged = ScriptableImGui::SliderFloat("Scale UV", &m_planeScaleUV, 0.f, 2.f);
+                    if (scaleChanged)
+                    {
+                        m_parallaxMaterial->SetPropertyValue(m_scaleUVIndex, m_planeScaleUV);
+                    }
+
+                    planeUVChanged = centerUChanged || centerVChanged || tileUChanged || tileVChanged || offsetUChanged || offsetVChanged || rotationUVChanged || scaleChanged;
+                }
+                ImGui::Unindent();
+            }
+
+            m_imguiSidebar.End();
+
+            if (parallaxSettingChanged || planeUVChanged)
+            {
+                m_parallaxMaterial->Compile();
+            }
+        }
+    }
+
+}

+ 132 - 0
Gem/Code/Source/ParallaxMappingExampleComponent.h

@@ -0,0 +1,132 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+#include <CommonSampleComponentBase.h>
+#include <Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h>
+#include <Atom/Feature/CoreLights/SpotLightFeatureProcessorInterface.h>
+#include <AzCore/Component/TickBus.h>
+
+#include <Utils/ImGuiSidebar.h>
+#include <Utils/Utils.h>
+
+namespace AtomSampleViewer
+{
+    //! Demostrate the effect of Parallax Mapping and Pixel Depth Offset
+    class ParallaxMappingExampleComponent final
+        : public CommonSampleComponentBase
+        , public AZ::TickBus::Handler
+    {
+    public:
+
+        AZ_COMPONENT(ParallaxMappingExampleComponent, "{C2530F5A-8626-49B7-8913-DDEA25C7E7CD}", CommonSampleComponentBase);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        ParallaxMappingExampleComponent();
+        ~ParallaxMappingExampleComponent() override = default;
+
+        void Activate() override;
+        void Deactivate() override;
+
+    private:
+        // AZ::TickBus::Handler overrides...
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time);
+
+        static constexpr float ConeAngleInnerRatio = 0.9f;
+        static constexpr float CutoffIntensity = 0.1f;
+
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle LoadMesh(
+            AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset,
+            AZ::Data::Instance<AZ::RPI::Material> material,
+            AZ::Transform transform);
+        void CreateDirectionalLight();
+        void CreateSpotLight();
+        void DrawSidebar();
+
+        void SaveCameraConfiguration();
+        void RestoreCameraConfiguration();
+        void SetCameraConfiguration();
+        void ConfigureCameraToLookDown();
+
+        // Mesh Handles
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_planeHandle;
+        AZ::Render::MeshFeatureProcessorInterface::MeshHandle m_boxHandle;
+
+        // Light
+        AZ::Render::DirectionalLightFeatureProcessorInterface* m_directionalLightFeatureProcessor = nullptr;
+        AZ::Render::SpotLightFeatureProcessorInterface* m_spotLightFeatureProcessor = nullptr;
+        AZ::Render::DirectionalLightFeatureProcessorInterface::LightHandle m_directionalLightHandle;
+        AZ::Render::SpotLightFeatureProcessorInterface::LightHandle m_spotLightHandle;
+
+        float m_lightRotationAngle = 0.f; // in radian
+        bool m_lightAutoRotate = true;
+        int m_lightType = 0; // 0: diectionalLight, 1: spotLight
+
+        //Assets
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_planeAsset;
+        AZ::Data::Asset<AZ::RPI::ModelAsset> m_boxAsset;
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_defaultMaterialAsset;
+        AZ::Data::Asset<AZ::RPI::MaterialAsset> m_parallaxMaterialAsset;
+        AZ::Data::Instance<AZ::RPI::Material> m_defaultMaterial;
+        AZ::Data::Instance<AZ::RPI::Material> m_parallaxMaterial;
+
+        AZ::RPI::MaterialPropertyIndex m_parallaxEnableIndex;
+        AZ::RPI::MaterialPropertyIndex m_parallaxFactorIndex;
+        AZ::RPI::MaterialPropertyIndex m_parallaxAlgorithmIndex;
+        AZ::RPI::MaterialPropertyIndex m_parallaxQualityIndex;
+        AZ::RPI::MaterialPropertyIndex m_parallaxUvIndex;
+        AZ::RPI::MaterialPropertyIndex m_pdoEnableIndex;
+
+        // Other UVs that when parallaxUv is changed, they must follow the change.
+        AZ::RPI::MaterialPropertyIndex m_ambientOcclusionUvIndex;
+        AZ::RPI::MaterialPropertyIndex m_baseColorUvIndex;
+        AZ::RPI::MaterialPropertyIndex m_normalUvIndex;
+        AZ::RPI::MaterialPropertyIndex m_roughnessUvIndex;
+
+        AZ::RPI::MaterialPropertyIndex m_centerUVIndex;
+        AZ::RPI::MaterialPropertyIndex m_tileUIndex;
+        AZ::RPI::MaterialPropertyIndex m_tileVIndex;
+        AZ::RPI::MaterialPropertyIndex m_scaleUVIndex;
+        AZ::RPI::MaterialPropertyIndex m_offsetUIndex;
+        AZ::RPI::MaterialPropertyIndex m_offsetVIndex;
+        AZ::RPI::MaterialPropertyIndex m_rotationUVIndex;
+
+        // parallax setting
+        bool m_parallaxEnable = true;
+        bool m_pdoEnable = true;
+        float m_parallaxFactor = 0.03f;
+        // see StandardPbr.materialtype for the full enum list.
+        int m_parallaxAlgorithm = 2; // POM
+        int m_parallaxQuality = 2;   // High
+        int m_parallaxUv = 0; // 0 = UV0, 1 = UV1
+
+        // plane setting
+        AZ::Transform m_planeTransform;
+        float m_planeRotationAngle = 0.f; // in radian
+        float m_planeCenterU = 0.f;
+        float m_planeCenterV = 0.f;
+        float m_planeTileU = 1.f;
+        float m_planeTileV = 1.f;
+        float m_planeScaleUV = 1.f;
+        float m_planeOffsetU = 0.f;
+        float m_planeOffsetV = 0.f;
+        float m_planeRotateUV = 0.f; // in degrees
+
+        // original camera configuration
+        float m_originalFarClipDistance = 0.f;
+        float m_originalCameraFovRadians = 0.f;
+
+        // ImGui
+        ImGuiSidebar m_imguiSidebar;
+    };
+} // namespace AtomSampleViewer

+ 21 - 0
Gem/Code/Source/Platform/Android/AtomSampleViewerOptions_Android.cpp

@@ -0,0 +1,21 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MultiRenderPipelineExampleComponent.h>
+
+namespace AtomSampleViewer
+{
+    bool SupportsMultipleWindows()
+    {
+        return false;
+    }
+} // namespace AtomSampleViewer

+ 13 - 0
Gem/Code/Source/Platform/Android/MultiThreadComponent_Traits_Platform.h

@@ -0,0 +1,13 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#define ATOMSAMPLEVIEWER_TRAIT_MULTITHREAD_SAMPLE_CUBES_PER_LINE 40

+ 35 - 0
Gem/Code/Source/Platform/Android/SampleComponentManager_Android.cpp

@@ -0,0 +1,35 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <SampleComponentManager.h>
+
+namespace AtomSampleViewer
+{
+    bool SampleComponentManager::IsMultiViewportSwapchainSampleSupported()
+    {
+        return false;
+    }
+
+    void SampleComponentManager::AdjustImGuiFontScale()
+    {
+        // Scale the text and the general size for mobile platforms because the screens are smaller.
+        const float fontScale = 1.5f;
+        const float sizeScale = 2.0f;
+        AZ::Render::ImGuiSystemRequestBus::Broadcast(&AZ::Render::ImGuiSystemRequestBus::Events::SetGlobalSizeScale, sizeScale);
+        AZ::Render::ImGuiSystemRequestBus::Broadcast(&AZ::Render::ImGuiSystemRequestBus::Events::SetGlobalFontScale, fontScale);
+    }
+
+    const char* SampleComponentManager::GetRootPassTemplateName()
+    {
+        return "MainPipeline_Mobile";
+    }
+} // namespace AtomSampleViewer

+ 21 - 0
Gem/Code/Source/Platform/Android/StreamingImageExampleComponent_Android.cpp

@@ -0,0 +1,21 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <StreamingImageExampleComponent.h>
+
+namespace AtomSampleViewer
+{
+    bool StreamingImageExampleComponent::IsHotReloadTestSupported()
+    {
+        return false;
+    }
+}

+ 30 - 0
Gem/Code/Source/Platform/Android/Utils_Android.cpp

@@ -0,0 +1,30 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+#include <Utils/Utils.h>
+
+#include <AzCore/PlatformIncl.h>
+
+namespace AtomSampleViewer
+{
+    namespace Utils
+    {
+        bool SupportsResizeClientArea()
+        {
+            return false;
+        }
+
+        bool RunDiffTool(const AZStd::string& filePathA, const AZStd::string& filePathB)
+        {
+            return false;
+        }
+    } // namespace Utils
+} // namespace AtomSampleViewer

+ 22 - 0
Gem/Code/Source/Platform/Android/additional_android_runtime_library.cmake

@@ -0,0 +1,22 @@
+#
+# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+# its licensors.
+#
+# For complete copyright and license terms please see the LICENSE at the root of this
+# distribution (the "License"). All use of this software is governed by the License,
+# or, if provided, by the license below or the license accompanying this file. Do not
+# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#
+
+set(LY_RUNTIME_DEPENDENCIES
+    3rdParty::VkValidation
+)
+
+set(VKVALIDATION_VERSION
+    r21d
+)
+
+set(VKVALIDATION_3RDPARTY_PLATFORM_DIRECTORY
+    android-ndk
+)

+ 18 - 0
Gem/Code/Source/Platform/Android/atomsampleviewer_android_files.cmake

@@ -0,0 +1,18 @@
+#
+# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+# its licensors.
+#
+# For complete copyright and license terms please see the LICENSE at the root of this
+# distribution (the "License"). All use of this software is governed by the License,
+# or, if provided, by the license below or the license accompanying this file. Do not
+# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#
+
+set(FILES
+    AtomSampleViewerOptions_Android.cpp
+    MultiThreadComponent_Traits_Platform.h
+    SampleComponentManager_Android.cpp
+    StreamingImageExampleComponent_Android.cpp
+    Utils_Android.cpp
+)

+ 21 - 0
Gem/Code/Source/Platform/Linux/AtomSampleViewerOptions_Linux.cpp

@@ -0,0 +1,21 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <MultiRenderPipelineExampleComponent.h>
+
+namespace AtomSampleViewer
+{
+    bool SupportsMultipleWindows()
+    {
+        return true;
+    }
+} // namespace AtomSampleViewer

+ 13 - 0
Gem/Code/Source/Platform/Linux/MultiThreadComponent_Traits_Platform.h

@@ -0,0 +1,13 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#define ATOMSAMPLEVIEWER_TRAIT_MULTITHREAD_SAMPLE_CUBES_PER_LINE 100

+ 30 - 0
Gem/Code/Source/Platform/Linux/SampleComponentManager_Linux.cpp

@@ -0,0 +1,30 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <SampleComponentManager.h>
+
+namespace AtomSampleViewer
+{
+    bool SampleComponentManager::IsMultiViewportSwapchainSampleSupported()
+    {
+        return true;
+    }
+
+    void SampleComponentManager::AdjustImGuiFontScale()
+    {
+    }
+
+    const char* SampleComponentManager::GetRootPassTemplateName()
+    {
+        return "MainPipeline";
+    }
+} // namespace AtomSampleViewer

+ 21 - 0
Gem/Code/Source/Platform/Linux/StreamingImageExampleComponent_Linux.cpp

@@ -0,0 +1,21 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include <StreamingImageExampleComponent.h>
+
+namespace AtomSampleViewer
+{
+    bool StreamingImageExampleComponent::IsHotReloadTestSupported()
+    {
+        return true;
+    }
+}

+ 30 - 0
Gem/Code/Source/Platform/Linux/Utils_Linux.cpp

@@ -0,0 +1,30 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+#include <Utils/Utils.h>
+
+#include <AzCore/PlatformIncl.h>
+
+namespace AtomSampleViewer
+{
+    namespace Utils
+    {
+        bool SupportsResizeClientArea()
+        {
+            return false;
+        }
+
+        bool RunDiffTool(const AZStd::string& filePathA, const AZStd::string& filePathB)
+        {
+            return false;
+        }
+    } // namespace Utils
+} // namespace AtomSampleViewer

+ 10 - 0
Gem/Code/Source/Platform/Linux/additional_linux_runtime_library.cmake

@@ -0,0 +1,10 @@
+#
+# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+# its licensors.
+#
+# For complete copyright and license terms please see the LICENSE at the root of this
+# distribution (the "License"). All use of this software is governed by the License,
+# or, if provided, by the license below or the license accompanying this file. Do not
+# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#

Some files were not shown because too many files changed in this diff