Browse Source

Add support for a XR sample using Oculus link (#3)

* Add support for a XR sample which does the following
- Create a new RHI sample for OpenXr
- Update ASV to support a simple VR pipeline that creates two pipelines (one for the left eye and one for the right eye)
- Add extracting view data per view index and using to the appropriate view projection matrix per eye
- The sample pass renders 3 cubes -> One cube per controller and another to show the view in front of the device
- Update controller position, orientation and scale

Signed-off-by: moudgils <[email protected]>

* - Add support for a depth buffer and depth clipping
- Address feedback
- Fix RPI samples

Signed-off-by: moudgils <[email protected]>

* Address minor feedback

Signed-off-by: moudgils <[email protected]>
Signed-off-by: amzn-phist <[email protected]>
moudgils 3 years ago
parent
commit
5e2e8c7fac

+ 5 - 0
Gem/Code/Source/Platform/Windows/additional_windows_runtime_library.cmake

@@ -5,3 +5,8 @@
 # SPDX-License-Identifier: Apache-2.0 OR MIT
 #
 #
+
+set(LY_RUNTIME_DEPENDENCIES
+    Gem::XR
+    Gem::OpenXRVk
+)

+ 16 - 1
Gem/Code/Source/RHI/BasicRHIComponent.cpp

@@ -57,7 +57,7 @@ namespace AtomSampleViewer
         // The RHISamplePass template should have one owned image attachment which is the render target
         m_outputAttachment = m_ownedAttachments[0];
 
-        // Force udpate pass attachment to get currect size and save it to local variables
+        // Force update pass attachment to get correct size and save it to local variables
         m_outputAttachment->Update();
 
         // update output info for the rhi sample
@@ -90,6 +90,16 @@ namespace AtomSampleViewer
         }
     }
 
+    uint32_t RHISamplePass::GetViewIndex() const
+    {
+        return m_viewIndex;
+    }
+
+    void RHISamplePass::SetViewIndex(const uint32_t viewIndex)
+    {
+        m_viewIndex = viewIndex;
+    }
+
     bool BasicRHIComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
     {
         using namespace AZ;
@@ -610,4 +620,9 @@ namespace AtomSampleViewer
     {
         return m_viewport.m_maxY - m_viewport.m_minY;
     }
+
+    void BasicRHIComponent::SetViewIndex(const uint32_t viewIndex)
+    {
+        m_viewIndex = viewIndex;
+    }
 }

+ 12 - 0
Gem/Code/Source/RHI/BasicRHIComponent.h

@@ -66,6 +66,10 @@ namespace AtomSampleViewer
         // Pass overrides
         const AZ::RPI::PipelineViewTag& GetPipelineViewTag() const override;
 
+        // Return the view index of the pass
+        uint32_t GetViewIndex() const;
+        void SetViewIndex(const uint32_t viewIndex);
+
     protected:
         explicit RHISamplePass(const AZ::RPI::PassDescriptor& descriptor);
 
@@ -78,6 +82,9 @@ namespace AtomSampleViewer
         AZ::RPI::Ptr<AZ::RPI::PassAttachment> m_outputAttachment;
 
         AZ::RPI::PipelineViewTag m_pipelineViewTag;
+
+        // Used to determine view index for XR sample
+        uint32_t m_viewIndex = 0;
     };
 
     class BasicRHIComponent
@@ -103,6 +110,8 @@ namespace AtomSampleViewer
         
         float GetViewportHeight();
         
+        void SetViewIndex(const uint32_t viewIndex);
+       
     protected:
         AZ_DISABLE_COPY(BasicRHIComponent);
 
@@ -237,5 +246,8 @@ namespace AtomSampleViewer
 
         // whether this sample supports RHI sample render pipeline or not
         bool m_supportRHISamplePipeline = false;
+        
+        // view index. Used by XR related samples
+        uint32_t m_viewIndex = 0;
     };
 } // namespace AtomSampleViewer

+ 394 - 0
Gem/Code/Source/RHI/XRExampleComponent.cpp

@@ -0,0 +1,394 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <RHI/XRExampleComponent.h>
+#include <Atom/RHI/CommandList.h>
+#include <Atom/RHI.Reflect/InputStreamLayoutBuilder.h>
+#include <Atom/RHI.Reflect/RenderAttachmentLayoutBuilder.h>
+#include <Atom/RPI.Public/Shader/Shader.h>
+#include <Atom/RPI.Reflect/Shader/ShaderAsset.h>
+#include <AzCore/Math/Color.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <SampleComponentManager.h>
+#include <Utils/Utils.h>
+
+namespace AtomSampleViewer
+{
+    void XRExampleComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<XRExampleComponent, AZ::Component>()->Version(0);
+        }
+    }
+
+    XRExampleComponent::XRExampleComponent()
+    {
+        m_supportRHISamplePipeline = true;
+        
+    }
+
+    void XRExampleComponent::Activate()
+    {
+        m_depthStencilID = AZ::RHI::AttachmentId{ AZStd::string::format("DepthStencilID_% u", GetId()) };
+        CreateCubeInputAssemblyBuffer();
+        CreateCubePipeline();
+        CreateScope();
+        AZ::RHI::RHISystemNotificationBus::Handler::BusConnect();
+        AZ::TickBus::Handler::BusConnect();
+    }
+
+    void XRExampleComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
+    {
+        m_time += deltaTime;
+    }
+
+    void XRExampleComponent::OnFramePrepare(AZ::RHI::FrameGraphBuilder& frameGraphBuilder)
+    {
+        AZ::Matrix4x4 projection = AZ::Matrix4x4::CreateIdentity();
+        AZ::Matrix4x4 vpMat = AZ::Matrix4x4::CreateIdentity();
+
+        AZ::RPI::XRRenderingInterface* xrSystem = AZ::RPI::RPISystemInterface::Get()->GetXRSystem();
+        if (xrSystem && xrSystem->ShouldRender())
+        {
+            const AZ::RPI::FovData fovData = xrSystem->GetViewFov(m_viewIndex);
+            const AZ::RPI::PoseData poseData = xrSystem->GetViewPose(m_viewIndex);
+                
+            static const float clip_near = 0.05f;
+            static const float clip_far = 100.0f;
+            projection = xrSystem->CreateProjectionOffset(fovData.m_angleLeft, fovData.m_angleRight, 
+                                                          fovData.m_angleDown, fovData.m_angleUp, 
+                                                          clip_near, clip_far);
+
+            AZ::Quaternion poseOrientation = poseData.orientation; 
+            poseOrientation.InvertFast(); 
+            AZ::Matrix4x4 viewMat = AZ::Matrix4x4::CreateFromQuaternionAndTranslation(poseOrientation, -poseData.position);
+            m_viewProjMatrix = projection * viewMat;
+ 
+            const AZ::Matrix4x4 initialScaleMat = AZ::Matrix4x4::CreateScale(AZ::Vector3(0.1f, 0.1f, 0.1f));
+
+            //Model matrix for the cube related to the front view
+            AZ::RPI::PoseData frontViewPoseData = xrSystem->GetViewFrontPose();
+            m_modelMatrices[0] = AZ::Matrix4x4::CreateFromQuaternionAndTranslation(frontViewPoseData.orientation, frontViewPoseData.position) * initialScaleMat;
+                      
+            //Model matrix for the cube related to the left controller
+            AZ::RPI::PoseData controllerLeftPose = xrSystem->GetControllerPose(0);
+            AZ::Matrix4x4 leftScaleMat = initialScaleMat * AZ::Matrix4x4::CreateScale(AZ::Vector3(xrSystem->GetControllerScale(0)));
+            m_modelMatrices[1] = AZ::Matrix4x4::CreateFromQuaternionAndTranslation(controllerLeftPose.orientation, controllerLeftPose.position) * leftScaleMat;
+
+            //Model matrix for the cube related to the right controller
+            AZ::Matrix4x4 rightScaleMat = initialScaleMat * AZ::Matrix4x4::CreateScale(AZ::Vector3(xrSystem->GetControllerScale(1)));
+            AZ::RPI::PoseData controllerRightPose = xrSystem->GetControllerPose(1);
+            m_modelMatrices[2] = AZ::Matrix4x4::CreateFromQuaternionAndTranslation(controllerRightPose.orientation, controllerRightPose.position) * rightScaleMat;
+        }      
+        
+        for (int i = 0; i < NumberOfCubes; ++i)
+        {
+            m_shaderResourceGroups[i]->SetConstant(m_shaderIndexWorldMat, m_modelMatrices[i]);
+            m_shaderResourceGroups[i]->SetConstant(m_shaderIndexViewProj, m_viewProjMatrix);
+            m_shaderResourceGroups[i]->Compile();
+        }
+
+        BasicRHIComponent::OnFramePrepare(frameGraphBuilder);
+    }
+
+    XRExampleComponent::SingleCubeBufferData XRExampleComponent::CreateSingleCubeBufferData()
+    {
+        const AZStd::fixed_vector<AZ::Color, GeometryVertexCount> vertexColor =
+        {
+            //Front Face
+            AZ::Colors::DarkBlue,   AZ::Colors::DarkBlue,   AZ::Colors::DarkBlue,   AZ::Colors::DarkBlue,
+            //Back Face                                                                       
+            AZ::Colors::Blue,       AZ::Colors::Blue,       AZ::Colors::Blue,       AZ::Colors::Blue,
+            //Left Face                                                                      
+            AZ::Colors::DarkGreen,  AZ::Colors::DarkGreen,  AZ::Colors::DarkGreen,  AZ::Colors::DarkGreen,
+            //Right Face                                                                    
+            AZ::Colors::Green,      AZ::Colors::Green,      AZ::Colors::Green,      AZ::Colors::Green,
+            //Top Face                                                                  
+            AZ::Colors::DarkRed,    AZ::Colors::DarkRed,    AZ::Colors::DarkRed,    AZ::Colors::DarkRed,
+            //Bottom Face                                                                    
+            AZ::Colors::Red,        AZ::Colors::Red,        AZ::Colors::Red,        AZ::Colors::Red,
+        };
+
+        // Create vertices, colors and normals for a cube and a plane
+        SingleCubeBufferData bufferData;
+        {
+            
+            const AZStd::fixed_vector<AZ::Vector3, GeometryVertexCount> vertices =
+            {
+                //Front Face
+                AZ::Vector3(1.0, 1.0, 1.0),         AZ::Vector3(-1.0, 1.0, 1.0),     AZ::Vector3(-1.0, -1.0, 1.0),    AZ::Vector3(1.0, -1.0, 1.0),
+                //Back Face                                                                       
+                AZ::Vector3(1.0, 1.0, -1.0),        AZ::Vector3(-1.0, 1.0, -1.0),    AZ::Vector3(-1.0, -1.0, -1.0),   AZ::Vector3(1.0, -1.0, -1.0),
+                //Left Face                                                                      
+                AZ::Vector3(-1.0, 1.0, 1.0),        AZ::Vector3(-1.0, -1.0, 1.0),    AZ::Vector3(-1.0, -1.0, -1.0),   AZ::Vector3(-1.0, 1.0, -1.0),
+                //Right Face                                                                    
+                AZ::Vector3(1.0, 1.0, 1.0),         AZ::Vector3(1.0, -1.0, 1.0),     AZ::Vector3(1.0, -1.0, -1.0),    AZ::Vector3(1.0, 1.0, -1.0),
+                //Top Face                                                                  
+                AZ::Vector3(1.0, 1.0, 1.0),         AZ::Vector3(-1.0, 1.0, 1.0),     AZ::Vector3(-1.0, 1.0, -1.0),    AZ::Vector3(1.0, 1.0, -1.0),
+                //Bottom Face                                                                    
+                AZ::Vector3(1.0, -1.0, 1.0),        AZ::Vector3(-1.0, -1.0, 1.0),    AZ::Vector3(-1.0, -1.0, -1.0),   AZ::Vector3(1.0, -1.0, -1.0),
+            };
+
+            for (int i = 0; i < GeometryVertexCount; ++i)
+            {
+                SetVertexPosition(bufferData.m_positions.data(), i, vertices[i]);
+                SetVertexColor(bufferData.m_colors.data(), i, vertexColor[i].GetAsVector4());
+            }
+
+            bufferData.m_indices =
+            {
+                {
+                    //Back
+                    2, 0, 1,
+                    0, 2, 3,
+                    //Front
+                    4, 6, 5,
+                    6, 4, 7,
+                    //Left
+                    8, 10, 9,
+                    10, 8, 11,
+                    //Right
+                    14, 12, 13,
+                    15, 12, 14,
+                    //Top
+                    16, 18, 17,
+                    18, 16, 19,
+                    //Bottom
+                    22, 20, 21,
+                    23, 20, 22,
+                }
+            };
+        }
+        return bufferData;
+    }
+
+    void XRExampleComponent::CreateCubeInputAssemblyBuffer()
+    {
+        const AZ::RHI::Ptr<AZ::RHI::Device> device = Utils::GetRHIDevice();
+        AZ::RHI::ResultCode result = AZ::RHI::ResultCode::Success;
+
+        m_bufferPool = AZ::RHI::Factory::Get().CreateBufferPool();
+        AZ::RHI::BufferPoolDescriptor bufferPoolDesc;
+        bufferPoolDesc.m_bindFlags = AZ::RHI::BufferBindFlags::InputAssembly;
+        bufferPoolDesc.m_heapMemoryLevel = AZ::RHI::HeapMemoryLevel::Device;
+        result = m_bufferPool->Init(*device, bufferPoolDesc);
+        if (result != AZ::RHI::ResultCode::Success)
+        {
+            AZ_Error("XRExampleComponent", false, "Failed to initialize buffer pool with error code %d", result);
+            return;
+        }
+
+        SingleCubeBufferData bufferData = CreateSingleCubeBufferData();
+
+        m_inputAssemblyBuffer = AZ::RHI::Factory::Get().CreateBuffer();
+        AZ::RHI::BufferInitRequest request;
+
+        request.m_buffer = m_inputAssemblyBuffer.get();
+        request.m_descriptor = AZ::RHI::BufferDescriptor{ AZ::RHI::BufferBindFlags::InputAssembly, sizeof(SingleCubeBufferData) };
+        request.m_initialData = &bufferData;
+        result = m_bufferPool->InitBuffer(request);
+        if (result != AZ::RHI::ResultCode::Success)
+        {
+            AZ_Error("XRExampleComponent", false, "Failed to initialize buffer with error code %d", result);
+            return;
+        }
+
+        m_streamBufferViews[0] =
+        {
+            *m_inputAssemblyBuffer,
+            offsetof(SingleCubeBufferData, m_positions),
+            sizeof(SingleCubeBufferData::m_positions),
+            sizeof(VertexPosition)
+        };
+
+        m_streamBufferViews[1] =
+        {
+            *m_inputAssemblyBuffer,
+            offsetof(SingleCubeBufferData, m_colors),
+            sizeof(SingleCubeBufferData::m_colors),
+            sizeof(VertexColor)
+        };
+
+        m_indexBufferView =
+        {
+            *m_inputAssemblyBuffer,
+            offsetof(SingleCubeBufferData, m_indices),
+            sizeof(SingleCubeBufferData::m_indices),
+            AZ::RHI::IndexFormat::Uint16
+        };
+
+        AZ::RHI::InputStreamLayoutBuilder layoutBuilder;
+        layoutBuilder.SetTopology(AZ::RHI::PrimitiveTopology::TriangleList);
+        layoutBuilder.AddBuffer()->Channel("POSITION", AZ::RHI::Format::R32G32B32_FLOAT);
+        layoutBuilder.AddBuffer()->Channel("COLOR", AZ::RHI::Format::R32G32B32A32_FLOAT);
+        m_streamLayoutDescriptor.Clear();
+        m_streamLayoutDescriptor = layoutBuilder.End();
+
+        AZ::RHI::ValidateStreamBufferViews(m_streamLayoutDescriptor, m_streamBufferViews);
+    }
+
+    void XRExampleComponent::CreateCubePipeline()
+    {
+        const char* shaderFilePath = "Shaders/RHI/OpenXrSample.azshader";
+        const char* sampleName = "XRExampleComponent";
+
+        auto shader = LoadShader(shaderFilePath, sampleName);
+        if (shader == nullptr)
+        { 
+            return;
+        }
+
+        const AZ::RHI::Ptr<AZ::RHI::Device> device = Utils::GetRHIDevice();
+        AZ::RHI::PipelineStateDescriptorForDraw pipelineDesc;
+        shader->GetVariant(AZ::RPI::ShaderAsset::RootShaderVariantStableId).ConfigurePipelineState(pipelineDesc);
+        pipelineDesc.m_inputStreamLayout = m_streamLayoutDescriptor;
+        pipelineDesc.m_renderStates.m_depthStencilState.m_depth.m_enable = 1;
+        pipelineDesc.m_renderStates.m_depthStencilState.m_depth.m_func = AZ::RHI::ComparisonFunc::LessEqual;
+
+        AZ::RHI::RenderAttachmentLayoutBuilder attachmentsBuilder;
+        attachmentsBuilder.AddSubpass()
+            ->RenderTargetAttachment(m_outputFormat)
+            ->DepthStencilAttachment(device->GetNearestSupportedFormat(AZ::RHI::Format::D24_UNORM_S8_UINT, AZ::RHI::FormatCapabilities::DepthStencil));
+            
+        [[maybe_unused]] AZ::RHI::ResultCode result = attachmentsBuilder.End(pipelineDesc.m_renderAttachmentConfiguration.m_renderAttachmentLayout);
+        AZ_Assert(result == AZ::RHI::ResultCode::Success, "Failed to create render attachment layout");
+
+        m_pipelineState = shader->AcquirePipelineState(pipelineDesc);
+        if (!m_pipelineState)
+        {
+            AZ_Error("XRExampleComponent", false, "Failed to acquire default pipeline state for shader '%s'", shaderFilePath);
+            return;
+        }
+
+        auto perInstanceSrgLayout = shader->FindShaderResourceGroupLayout(AZ::Name{ "OpenXrSrg" });
+        if (!perInstanceSrgLayout)
+        {
+            AZ_Error("XRExampleComponent", false, "Failed to get shader resource group layout");
+            return;
+        }
+
+        for (int i = 0; i < NumberOfCubes; ++i)
+        {
+            m_shaderResourceGroups[i] = CreateShaderResourceGroup(shader, "OpenXrSrg", sampleName);
+        }
+
+        // Using the first SRG to get the correct index as all the SRGs will have the same indices.
+        FindShaderInputIndex(&m_shaderIndexWorldMat, m_shaderResourceGroups[0], AZ::Name{ "m_worldMatrix" }, "XRExampleComponent");
+        FindShaderInputIndex(&m_shaderIndexViewProj, m_shaderResourceGroups[0], AZ::Name{ "m_viewProjMatrix" }, "XRExampleComponent");
+    }
+
+    void XRExampleComponent::CreateScope()
+    {
+        // Creates a scope for rendering the triangle.
+        struct ScopeData
+        {
+
+        };
+        const auto prepareFunction = [this](AZ::RHI::FrameGraphInterface frameGraph, [[maybe_unused]] ScopeData& scopeData)
+        {
+            // Binds the swap chain as a color attachment. Clears it to black.
+            {
+                AZ::RHI::ImageScopeAttachmentDescriptor descriptor;
+                descriptor.m_attachmentId = m_outputAttachmentId;
+                descriptor.m_loadStoreAction.m_loadAction = AZ::RHI::AttachmentLoadAction::Load;
+                frameGraph.UseColorAttachment(descriptor);
+            }
+
+            // Create & Binds DepthStencil image
+            {
+                const AZ::RHI::Ptr<AZ::RHI::Device> device = Utils::GetRHIDevice();
+                const AZ::RHI::ImageDescriptor imageDescriptor = AZ::RHI::ImageDescriptor::Create2D(
+                    AZ::RHI::ImageBindFlags::DepthStencil,
+                    m_outputWidth,
+                    m_outputHeight,
+                    device->GetNearestSupportedFormat(AZ::RHI::Format::D24_UNORM_S8_UINT, AZ::RHI::FormatCapabilities::DepthStencil));
+                const AZ::RHI::TransientImageDescriptor transientImageDescriptor(m_depthStencilID, imageDescriptor);
+
+                frameGraph.GetAttachmentDatabase().CreateTransientImage(transientImageDescriptor);
+
+                AZ::RHI::ImageScopeAttachmentDescriptor dsDesc;
+                dsDesc.m_attachmentId = m_depthStencilID;
+                dsDesc.m_imageViewDescriptor.m_overrideFormat = device->GetNearestSupportedFormat(AZ::RHI::Format::D24_UNORM_S8_UINT, AZ::RHI::FormatCapabilities::DepthStencil);
+                dsDesc.m_loadStoreAction.m_clearValue = AZ::RHI::ClearValue::CreateDepthStencil(1.0f, 0);
+                dsDesc.m_loadStoreAction.m_loadAction = AZ::RHI::AttachmentLoadAction::Clear;
+                dsDesc.m_loadStoreAction.m_loadActionStencil = AZ::RHI::AttachmentLoadAction::DontCare;
+                frameGraph.UseDepthStencilAttachment(dsDesc, AZ::RHI::ScopeAttachmentAccess::Write);
+            }
+
+            // We will submit NumberOfCubes draw items.
+            frameGraph.SetEstimatedItemCount(NumberOfCubes);
+        };
+
+        AZ::RHI::EmptyCompileFunction<ScopeData> compileFunction;
+
+        const auto executeFunction = [this](const AZ::RHI::FrameGraphExecuteContext& context, [[maybe_unused]] const ScopeData& scopeData)
+        {
+            AZ::RHI::CommandList* commandList = context.GetCommandList();
+
+            // Set persistent viewport and scissor state.
+            commandList->SetViewports(&m_viewport, 1);
+            commandList->SetScissors(&m_scissor, 1);
+
+            AZ::RHI::DrawIndexed drawIndexed;
+            drawIndexed.m_indexCount = GeometryIndexCount;
+            drawIndexed.m_instanceCount = 1;
+
+            // Dividing NumberOfCubes by context.GetCommandListCount() to balance to number 
+            // of draw call equally between each thread.
+            uint32_t numberOfCubesPerCommandList = NumberOfCubes / context.GetCommandListCount();
+            uint32_t indexStart = context.GetCommandListIndex() * numberOfCubesPerCommandList;
+            uint32_t indexEnd = indexStart + numberOfCubesPerCommandList;
+
+            if (context.GetCommandListIndex() == context.GetCommandListCount() - 1)
+            {
+                indexEnd = NumberOfCubes;
+            }
+
+            for (uint32_t i = indexStart; i < indexEnd; ++i)
+            {
+                const AZ::RHI::ShaderResourceGroup* shaderResourceGroups[] = { m_shaderResourceGroups[i]->GetRHIShaderResourceGroup() };
+
+                AZ::RHI::DrawItem drawItem;
+                drawItem.m_arguments = drawIndexed;
+                drawItem.m_pipelineState = m_pipelineState.get();
+                drawItem.m_indexBufferView = &m_indexBufferView;
+                drawItem.m_shaderResourceGroupCount = static_cast<uint8_t>(AZ::RHI::ArraySize(shaderResourceGroups));
+                drawItem.m_shaderResourceGroups = shaderResourceGroups;
+                drawItem.m_streamBufferViewCount = static_cast<uint8_t>(m_streamBufferViews.size());
+                drawItem.m_streamBufferViews = m_streamBufferViews.data();
+
+                commandList->Submit(drawItem);
+            }
+        };
+
+        m_scopeProducers.emplace_back(
+            aznew AZ::RHI::ScopeProducerFunction<
+            ScopeData,
+            decltype(prepareFunction),
+            decltype(compileFunction),
+            decltype(executeFunction)>(
+                AZ::RHI::ScopeId{ AZStd::string::format("XRSample_Id_% u", GetId()) },
+                ScopeData{},
+                prepareFunction,
+                compileFunction,
+                executeFunction));
+    }
+
+    void XRExampleComponent::Deactivate()
+    {
+        m_inputAssemblyBuffer = nullptr;
+        m_bufferPool = nullptr;
+        m_pipelineState = nullptr;
+        m_shaderResourceGroups.fill(nullptr);
+
+        AZ::RHI::RHISystemNotificationBus::Handler::BusDisconnect();
+        m_windowContext = nullptr;
+        m_scopeProducers.clear();
+    }
+} 

+ 101 - 0
Gem/Code/Source/RHI/XRExampleComponent.h

@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Component/Component.h>
+
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+
+#include <Atom/RHI/FrameScheduler.h>
+#include <Atom/RHI/DrawItem.h>
+#include <Atom/RHI/Device.h>
+#include <Atom/RHI/Factory.h>
+#include <Atom/RHI/PipelineState.h>
+#include <Atom/RHI/BufferPool.h>
+
+#include <AzCore/Math/Matrix4x4.h>
+
+#include <RHI/BasicRHIComponent.h>
+#include <AzCore/Component/TickBus.h>
+
+namespace AtomSampleViewer
+{
+    //! The purpose of this sample is to establish a simple XR sample utilizing a simple VR pipeline
+    //! It will render a mesh per controller plus one for the front view. It will prove out all the 
+    //! code related related to openxr device, instance, swapchain, session, input, space.       
+    class XRExampleComponent final
+        : public BasicRHIComponent
+        , public AZ::TickBus::Handler
+    {
+    public:
+        AZ_COMPONENT(XRExampleComponent, "{A7D9A921-1FF9-4078-92BD-169E258456E7}");
+        AZ_DISABLE_COPY(XRExampleComponent);
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        XRExampleComponent();
+        ~XRExampleComponent() override = default;
+
+    protected:
+        // 1 cube for view + 2 cubes for the controller
+        static const uint32_t NumberOfCubes = 3; 
+        static const uint32_t GeometryVertexCount = 24;
+        static const uint32_t GeometryIndexCount = 36;
+
+        struct SingleCubeBufferData
+        {
+            AZStd::array<VertexPosition, GeometryVertexCount> m_positions;
+            AZStd::array<VertexColor, GeometryVertexCount> m_colors;
+            AZStd::array<uint16_t, GeometryIndexCount> m_indices;
+        };
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+        // RHISystemNotificationBus::Handler
+        void OnFramePrepare(AZ::RHI::FrameGraphBuilder& frameGraphBuilder) override;
+
+        // TickBus::Handler
+        void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
+
+        //! Create IA data
+        void CreateCubeInputAssemblyBuffer();
+        //! Create Cube data
+        SingleCubeBufferData CreateSingleCubeBufferData();
+        //! Create PSO data
+        void CreateCubePipeline();
+        //! Create the relevant Scope
+        void CreateScope();
+
+        AZ::RHI::Ptr<AZ::RHI::BufferPool> m_bufferPool;
+        AZ::RHI::IndexBufferView m_indexBufferView;
+        AZ::RHI::Ptr<AZ::RHI::Buffer> m_inputAssemblyBuffer;
+        AZ::RHI::InputStreamLayout m_streamLayoutDescriptor;
+        AZ::RHI::ConstPtr<AZ::RHI::PipelineState> m_pipelineState;
+
+        struct BufferData
+        {
+            AZStd::array<VertexPosition, 3> m_positions;
+            AZStd::array<VertexColor, 3> m_colors;
+            AZStd::array<uint16_t, 3> m_indices;
+        };
+
+        AZStd::array<AZ::RHI::StreamBufferView, 2> m_streamBufferViews;
+        AZ::RHI::DrawItem m_drawItem;
+        float m_time = 0.0f;
+        AZStd::array<AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>, NumberOfCubes> m_shaderResourceGroups;
+        AZStd::array<AZ::Matrix4x4, NumberOfCubes> m_modelMatrices;
+        AZ::Matrix4x4 m_viewProjMatrix;
+
+        AZ::RHI::ShaderInputConstantIndex m_shaderIndexWorldMat;
+        AZ::RHI::ShaderInputConstantIndex m_shaderIndexViewProj;  
+        AZ::RHI::AttachmentId m_depthStencilID;
+    };
+} // namespace AtomSampleViewer

+ 123 - 34
Gem/Code/Source/SampleComponentManager.cpp

@@ -60,6 +60,7 @@
 #include <RHI/TextureExampleComponent.h>
 #include <RHI/TextureMapExampleComponent.h>
 #include <RHI/TriangleExampleComponent.h>
+#include <RHI/XRExampleComponent.h>
 #include <RHI/TrianglesConstantBufferExampleComponent.h>
 #include <RHI/BindlessPrototypeExampleComponent.h>
 #include <RHI/RayTracingExampleComponent.h>
@@ -305,6 +306,7 @@ namespace AtomSampleViewer
             NewRHISample<TextureMapExampleComponent>("TextureMap"),
             NewRHISample<TriangleExampleComponent>("Triangle"),
             NewRHISample<TrianglesConstantBufferExampleComponent>("TrianglesConstantBuffer"),
+            NewRHISample<XRExampleComponent>("OpenXr"),
             NewRHISample<MatrixAlignmentTestExampleComponent>("MatrixAlignmentTest"),
             NewRPISample<AssetLoadTestComponent>("AssetLoadTest"),
             NewRPISample<AuxGeomExampleComponent>("AuxGeom"),
@@ -571,6 +573,20 @@ namespace AtomSampleViewer
 
     void SampleComponentManager::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
     {
+        AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get();
+        if (rpiSystem->GetXRSystem())
+        {
+            //Only enable XR pipelines if the XR drivers indicate we have accurate pose information from the device
+            if (rpiSystem->GetXRSystem()->ShouldRender())
+            {
+                EnableXrPipelines();
+            }
+            else
+            {
+                DisableXrPipelines();
+            }
+        }
+
         if (m_imGuiFrameTimer)
         {
             m_imGuiFrameTimer->PushValue(deltaTime * 1000.0f);
@@ -1351,22 +1367,21 @@ namespace AtomSampleViewer
     {
         m_exampleEntity->Deactivate();
 
-        // Pointer to the m_activeSample must be nullified before m_activeSample is destroyed.
-        if (m_rhiSamplePass)
-        {
-            m_rhiSamplePass->SetRHISample(nullptr);
-        }
+        // Pointer to all the passes within m_rhiSamplePasses must be nullified before all the samples within m_activeSamples are destroyed.
+        SetRHISamplePass(nullptr);
 
-        if (m_activeSample != nullptr)
+        for (AZ::Component* activeComponent : m_activeSamples)
         {
-            // Disable the camera controller just in case the active sample enabled it and didn't disable in Deactivate().
-            AZ::Debug::CameraControllerRequestBus::Event(m_cameraEntity->GetId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
+            if (activeComponent != nullptr)
+            {
+                // Disable the camera controller just in case the active sample enabled it and didn't disable in Deactivate().
+                AZ::Debug::CameraControllerRequestBus::Event(m_cameraEntity->GetId(), &AZ::Debug::CameraControllerRequestBus::Events::Disable);
 
-            m_exampleEntity->RemoveComponent(m_activeSample);
-            delete m_activeSample;
+                m_exampleEntity->RemoveComponent(activeComponent);
+                delete activeComponent;
+            }
         }
-
-        m_activeSample = nullptr;
+        m_activeSamples.clear();
 
         // Force a reset of the shader variant finder to get more consistent testing of samples every time they are run, rather
         // than the first time for each sample being "special".
@@ -1382,7 +1397,7 @@ namespace AtomSampleViewer
 
         // Reset to RHI sample pipeline
         SwitchSceneForRHISample();
-        m_rhiSamplePass->SetRHISample(nullptr);
+        SetRHISamplePass(nullptr);
     }
 
     void SampleComponentManager::CreateDefaultCamera()
@@ -1503,24 +1518,33 @@ namespace AtomSampleViewer
             SwitchSceneForRPISample();
         }
 
-        SampleComponentConfig config(m_windowContext, m_cameraEntity->GetId(), m_entityContextId);
-        m_activeSample = m_exampleEntity->CreateComponent(sampleEntry.m_sampleUuid);
-        m_activeSample->SetConfiguration(config);
-
+        SampleComponentConfig config(m_windowContext, m_cameraEntity->GetId(), m_entityContextId); 
         // special setup for RHI samples
         if (sampleEntry.m_pipelineType == SamplePipelineType::RHI)
         {
-            BasicRHIComponent* rhiSampleComponent = static_cast<BasicRHIComponent*>(m_activeSample);
-            if (rhiSampleComponent->IsSupportedRHISamplePipeline())
-            {
-                m_rhiSamplePass->SetRHISample(rhiSampleComponent);
-            }
-            else
+            for (AZ::RPI::Ptr<RHISamplePass> samplePass : m_rhiSamplePasses)
             {
-                m_rhiSamplePass->SetRHISample(nullptr);
-            }
+                BasicRHIComponent* rhiSampleComponent = static_cast<BasicRHIComponent*>(m_exampleEntity->CreateComponent(sampleEntry.m_sampleUuid));
+                rhiSampleComponent->SetConfiguration(config);
+                rhiSampleComponent->SetViewIndex(samplePass->GetViewIndex());
+                if (rhiSampleComponent->IsSupportedRHISamplePipeline())
+                {
+                    samplePass->SetRHISample(rhiSampleComponent);
+                }
+                else
+                {
+                    samplePass->SetRHISample(nullptr);
+                }
+                m_activeSamples.push_back(rhiSampleComponent);
+            }   
         }
-
+        else
+        {
+            AZ::Component* newComponent = m_exampleEntity->CreateComponent(sampleEntry.m_sampleUuid);
+            newComponent->SetConfiguration(config);
+            m_activeSamples.push_back(newComponent);
+        }
+        
         m_exampleEntity->Activate();
 
         // Even though this is done in CameraReset(), the example component wasn't activated at the time so we have to send this event again.
@@ -1549,21 +1573,60 @@ namespace AtomSampleViewer
         m_rhiScene->Activate();
 
         RPI::RenderPipelineDescriptor pipelineDesc;
+
         pipelineDesc.m_name = "RHISamplePipeline";
         pipelineDesc.m_rootPassTemplate = "RHISamplePipelineTemplate";
         // Add view to pipeline since there are few RHI samples are using ViewSrg
         pipelineDesc.m_mainViewTagName = "MainCamera";
 
-        RPI::RenderPipelinePtr renderPipeline = RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext.get());
-        m_rhiScene->AddRenderPipeline(renderPipeline);
-        renderPipeline->SetDefaultViewFromEntity(m_cameraEntity->GetId());
-
-        RPI::RPISystemInterface::Get()->RegisterScene(m_rhiScene);
+        m_renderPipeline = RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext.get());
+        m_rhiScene->AddRenderPipeline(m_renderPipeline);
+        m_renderPipeline->SetDefaultViewFromEntity(m_cameraEntity->GetId());
+        
 
         // Get RHISamplePass
-        AZ::RPI::PassFilter passFilter = AZ::RPI::PassFilter::CreateWithPassName(AZ::Name("RHISamplePass"), renderPipeline.get());
-        m_rhiSamplePass = azrtti_cast<RHISamplePass*>(AZ::RPI::PassSystemInterface::Get()->FindFirstPass(passFilter));
+        AZ::RPI::PassFilter passFilter = AZ::RPI::PassFilter::CreateWithPassName(AZ::Name("RHISamplePass"), m_renderPipeline.get());
+        m_rhiSamplePasses.push_back(azrtti_cast<RHISamplePass*>(AZ::RPI::PassSystemInterface::Get()->FindFirstPass(passFilter)));
+
+        AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get();
+        if (rpiSystem->GetXRSystem())
+        {
+            // Build the pipeline for left eye
+            pipelineDesc.m_name = "RHISamplePipelineXRLeft";
+            pipelineDesc.m_rootPassTemplate = "RHISamplePipelineXRLeftTemplate";
+            RPI::RenderPipelinePtr renderPipelineLeft = RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext.get(), AZ::RPI::WindowContext::SwapChainMode::XrLeft);
+            // Build the pipeline for right eye
+            pipelineDesc.m_name = "RHISamplePipelineXRRight";
+            pipelineDesc.m_rootPassTemplate = "RHISamplePipelineXRRightTemplate";
+            RPI::RenderPipelinePtr renderPipelineRight = RPI::RenderPipeline::CreateRenderPipelineForWindow(pipelineDesc, *m_windowContext.get(), AZ::RPI::WindowContext::SwapChainMode::XrRight);
+
+            //Add both the pipelines to the scene
+            m_rhiScene->AddRenderPipeline(renderPipelineLeft);
+            m_rhiScene->AddRenderPipeline(renderPipelineRight);
+            renderPipelineLeft->SetDefaultViewFromEntity(m_cameraEntity->GetId());
+            renderPipelineRight->SetDefaultViewFromEntity(m_cameraEntity->GetId());
+            
+            // Set the correct view index for the RHI sample passes
+            AZ::RPI::PassFilter rhiSamplePassFilterLeft = AZ::RPI::PassFilter::CreateWithPassName(AZ::Name("RHISamplePass"), renderPipelineLeft.get());
+            AZ::RPI::Ptr<RHISamplePass> rhiSamplePassLeft = azrtti_cast<RHISamplePass*>(AZ::RPI::PassSystemInterface::Get()->FindFirstPass(rhiSamplePassFilterLeft));
+            rhiSamplePassLeft->SetViewIndex(0);
+            m_rhiSamplePasses.push_back(rhiSamplePassLeft);
+
+            AZ::RPI::PassFilter rhiSamplePassFilterRight = AZ::RPI::PassFilter::CreateWithPassName(AZ::Name("RHISamplePass"), renderPipelineRight.get());
+            AZ::RPI::Ptr<RHISamplePass> rhiSamplePassRight = azrtti_cast<RHISamplePass*>(AZ::RPI::PassSystemInterface::Get()->FindFirstPass(rhiSamplePassFilterRight));
+            rhiSamplePassRight->SetViewIndex(1);
+            m_rhiSamplePasses.push_back(rhiSamplePassRight);
+
+            //Cache the pipelines in case we want to enable/disable them
+            m_xrPipelines.push_back(renderPipelineLeft);
+            m_xrPipelines.push_back(renderPipelineRight);
 
+            //Disable XR pipelines by default
+            DisableXrPipelines();
+        }
+
+        // Register the RHi scene
+        RPI::RPISystemInterface::Get()->RegisterScene(m_rhiScene);  
         // Setup imGui since a new render pipeline with imgui pass was created
         SetupImGuiContext();
     }
@@ -1572,7 +1635,9 @@ namespace AtomSampleViewer
     {
         if (m_rhiScene)
         {
-            m_rhiSamplePass = nullptr;
+            m_rhiSamplePasses.clear();
+            m_xrPipelines.clear();
+            m_renderPipeline = nullptr;
             RPI::RPISystemInterface::Get()->UnregisterScene(m_rhiScene);
             m_rhiScene = nullptr;
         }
@@ -1684,5 +1749,29 @@ namespace AtomSampleViewer
                 ActivateInternal();
             });
     }
+    
+    void SampleComponentManager::SetRHISamplePass(BasicRHIComponent* sampleComponent)
+    {
+        for (AZ::RPI::Ptr<RHISamplePass> samplePass : m_rhiSamplePasses)
+        {
+            samplePass->SetRHISample(sampleComponent);
+        }
+    }
+
+    void SampleComponentManager::DisableXrPipelines()
+    {
+        for (RPI::RenderPipelinePtr xrPipeline : m_xrPipelines)
+        {
+            xrPipeline->RemoveFromRenderTick();
+        }
+    }
+
+    void SampleComponentManager::EnableXrPipelines()
+    {
+        for (RPI::RenderPipelinePtr xrPipeline : m_xrPipelines)
+        {
+            xrPipeline->AddToRenderTick();
+        }
+    }
 
 } // namespace AtomSampleViewer

+ 9 - 2
Gem/Code/Source/SampleComponentManager.h

@@ -148,6 +148,9 @@ namespace AtomSampleViewer
         void SampleChange();
         void CameraReset();
         void ShutdownActiveSample();
+        void SetRHISamplePass(BasicRHIComponent* sampleComponent);
+        void DisableXrPipelines();
+        void EnableXrPipelines();
 
         // SampleComponentManagerRequestBus overrides...
         void Reset() override;
@@ -192,7 +195,7 @@ namespace AtomSampleViewer
         // Entity to hold only example component. It doesn't need an entity context.
         AZ::Entity* m_exampleEntity = nullptr;
 
-        AZ::Component* m_activeSample = nullptr;
+        AZStd::vector<AZ::Component*> m_activeSamples;
 
         AZ::Entity* m_cameraEntity = nullptr;
 
@@ -260,12 +263,16 @@ namespace AtomSampleViewer
 
         // Scene and some variables for RHI samples
         AZ::RPI::ScenePtr m_rhiScene;
-        AZ::RPI::Ptr<RHISamplePass> m_rhiSamplePass = nullptr;
+        AZStd::vector<AZ::RPI::Ptr<RHISamplePass>> m_rhiSamplePasses;
 
         // Scene and some variables for RPI samples
         AZ::RPI::ScenePtr m_rpiScene;
 
         // number of MSAA samples, initialized in Activate() and can vary by platform
         int m_numMSAASamples = 0;
+
+        // Cache PC and XR pipelines
+        AZ::RPI::RenderPipelinePtr m_renderPipeline = nullptr;
+        AZStd::vector<AZ::RPI::RenderPipelinePtr> m_xrPipelines;
     };
 } // namespace AtomSampleViewer

+ 2 - 0
Gem/Code/atomsampleviewergem_private_files.cmake

@@ -85,6 +85,8 @@ set(FILES
     Source/RHI/RayTracingExampleComponent.h
     Source/RHI/MatrixAlignmentTestExampleComponent.cpp
     Source/RHI/MatrixAlignmentTestExampleComponent.h
+    Source/RHI/XRExampleComponent.cpp
+    Source/RHI/XRExampleComponent.h
     Source/Performance/HighInstanceExampleComponent.cpp
     Source/Performance/HighInstanceExampleComponent.h
     Source/Performance/100KDrawable_SingleView_ExampleComponent.cpp

+ 2 - 0
Gem/Code/enabled_gems.cmake

@@ -24,4 +24,6 @@ set(ENABLED_GEMS
     UiBasics
     StreamerProfiler
     DiffuseProbeGrid
+    XR
+    OpenXRVk
 )

+ 8 - 0
Passes/ASV/PassTemplates.azasset

@@ -36,6 +36,14 @@
                 "Name": "RHISamplePipelineTemplate",
                 "Path": "Passes/RHISamplePipeline.pass"
             },
+            {
+                "Name": "RHISamplePipelineXRLeftTemplate",
+                "Path": "Passes/RHISamplePipelineXRLeft.pass"
+            },
+            {
+                "Name": "RHISamplePipelineXRRightTemplate",
+                "Path": "Passes/RHISamplePipelineXRRight.pass"
+            },
             {
                 "Name": "SsaoPipeline",
                 "Path": "Passes/SsaoPipeline.pass"

+ 74 - 0
Passes/RHISamplePipelineXRLeft.pass

@@ -0,0 +1,74 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "PassAsset",
+    "ClassData": {
+        "PassTemplate": {
+            "Name": "RHISamplePipelineXRLeftTemplate",
+            "PassClass": "ParentPass",
+            "Slots": [
+                {
+                    "Name": "SwapChainOutput",
+                    "SlotType": "InputOutput"
+                }
+            ],
+            "PassData": {
+                "$type": "PassData",
+                "PipelineGlobalConnections": [
+                    {
+                        "GlobalName": "SwapChainOutput",
+                        "Slot": "SwapChainOutput"
+                    }
+                ]
+            },
+            "PassRequests": [
+                {
+                    "Name": "RHISamplePass",
+                    "TemplateName": "RHISamplePassTemplate",
+                    "Enabled": true,
+                    "ExecuteAfter": [
+                        "BeginXRPass"
+                    ]
+                },
+                {
+                    "Name": "ImGuiPass",
+                    "TemplateName": "ImGuiPassTemplate",
+                    "Enabled": true,
+                    "Connections": [
+                        {
+                            "LocalSlot": "InputOutput",
+                            "AttachmentRef": {
+                                "Pass": "RHISamplePass",
+                                "Attachment": "Output"
+                            }
+                        }
+                    ],
+                    "PassData": {
+                        "$type": "ImGuiPassData",
+                        "IsDefaultImGui": true
+                    }
+                },
+                {
+                    "Name": "CopyToSwapChain",
+                    "TemplateName": "FullscreenCopyTemplate",
+                    "Connections": [
+                        {
+                            "LocalSlot": "Input",
+                            "AttachmentRef": {
+                                "Pass": "ImGuiPass",
+                                "Attachment": "InputOutput"
+                            }
+                        },
+                        {
+                            "LocalSlot": "Output",
+                            "AttachmentRef": {
+                                "Pass": "Parent",
+                                "Attachment": "SwapChainOutput"
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    }
+}

+ 74 - 0
Passes/RHISamplePipelineXRRight.pass

@@ -0,0 +1,74 @@
+{
+    "Type": "JsonSerialization",
+    "Version": 1,
+    "ClassName": "PassAsset",
+    "ClassData": {
+        "PassTemplate": {
+            "Name": "RHISamplePipelineXRRightTemplate",
+            "PassClass": "ParentPass",
+            "Slots": [
+                {
+                    "Name": "SwapChainOutput",
+                    "SlotType": "InputOutput"
+                }
+            ],
+            "PassData": {
+                "$type": "PassData",
+                "PipelineGlobalConnections": [
+                    {
+                        "GlobalName": "SwapChainOutput",
+                        "Slot": "SwapChainOutput"
+                    }
+                ]
+            },
+            "PassRequests": [
+                {
+                    "Name": "RHISamplePass",
+                    "TemplateName": "RHISamplePassTemplate",
+                    "Enabled": true,
+                    "ExecuteAfter": [
+                        "BeginXRPass"
+                    ]
+                },
+                {
+                    "Name": "ImGuiPass",
+                    "TemplateName": "ImGuiPassTemplate",
+                    "Enabled": true,
+                    "Connections": [
+                        {
+                            "LocalSlot": "InputOutput",
+                            "AttachmentRef": {
+                                "Pass": "RHISamplePass",
+                                "Attachment": "Output"
+                            }
+                        }
+                    ],
+                    "PassData": {
+                        "$type": "ImGuiPassData",
+                        "IsDefaultImGui": true
+                    }
+                },
+                {
+                    "Name": "CopyToSwapChain",
+                    "TemplateName": "FullscreenCopyTemplate",
+                    "Connections": [
+                        {
+                            "LocalSlot": "Input",
+                            "AttachmentRef": {
+                                "Pass": "ImGuiPass",
+                                "Attachment": "InputOutput"
+                            }
+                        },
+                        {
+                            "LocalSlot": "Output",
+                            "AttachmentRef": {
+                                "Pass": "Parent",
+                                "Attachment": "SwapChainOutput"
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+    }
+}

+ 49 - 0
Shaders/RHI/OpenXrSample.azsl

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Atom/Features/SrgSemantics.azsli>
+
+ShaderResourceGroup OpenXrSrg : SRG_PerObject
+{
+    row_major float4x4 m_worldMatrix;
+    row_major float4x4 m_viewProjMatrix;
+}
+
+struct VSInput
+{
+    float3 m_position : POSITION;
+    float4 m_color : COLOR0;
+};
+
+struct VSOutput
+{
+    float4 m_position : SV_Position;
+    float4 m_color : COLOR0;
+};
+
+VSOutput MainVS(VSInput vsInput)
+{
+    VSOutput OUT;
+    
+    OUT.m_position = mul(OpenXrSrg::m_worldMatrix, float4(vsInput.m_position, 1.0));
+    OUT.m_position = mul(OpenXrSrg::m_viewProjMatrix, OUT.m_position);
+    OUT.m_color = vsInput.m_color;
+    return OUT;
+}
+
+struct PSOutput
+{
+    float4 m_color : SV_Target0;
+};
+
+PSOutput MainPS(VSOutput vsOutput)
+{
+    PSOutput OUT;
+    OUT.m_color = vsOutput.m_color;
+    return OUT;
+}

+ 23 - 0
Shaders/RHI/OpenXrSample.shader

@@ -0,0 +1,23 @@
+{
+    "Source" : "OpenXrSample.azsl",
+
+    "DepthStencilState" : { 
+        "Depth" : { "Enable" : false, "CompareFunc" : "Less" }
+    },
+    "DrawList" : "forward",
+
+    "ProgramSettings":
+    {
+      "EntryPoints":
+      [
+        {
+          "name": "MainVS",
+          "type": "Vertex"
+        },
+        {
+          "name": "MainPS",
+          "type": "Fragment"
+        }
+      ]
+    }
+}

+ 5 - 1
project.json

@@ -13,5 +13,9 @@
     ],
     "icon_path": "preview.png",
     "engine": "o3de",
-    "external_subdirectories": []
+    "external_subdirectories": [],
+    "gem_names": [
+        "XR",
+        "OpenXRVk"
+    ]
 }