Hello XR (OpenXR VR Application)¶
This example shows complete OpenXR/Vulkan integration for virtual reality applications. OpenXR is the industry-standard cross-platform VR API supporting all major headsets (Quest, Index, Vive, etc.). The example renders a 3D scene with multiple layer types: passthrough (real-world view), projection (3D scene), quad layers (flat UI panels), and cylinder layers (curved UI). This demonstrates the full OpenXR compositor system and device-independent VR input handling.
Overview¶
What this demonstrates: OpenXR initialization, VR compositor layers (passthrough/projection/quad/cylinder), device-independent input actions, per-eye view matrices, stereo rendering, ImGui VR overlays.
Use cases: VR games, VR training simulations, architectural visualization, VR tools.
Requirements¶
- Vulkan Version: 1.0+
- Extensions: VK_KHR_multiview recommended
- Runtime: OpenXR runtime (SteamVR, Oculus, Windows MR)
- Hardware: VR headset
Key Concepts¶
OpenXR: Cross-platform VR API abstracting hardware differences. Single codebase works on Quest, Index, Vive, etc.
Compositor Layers: Different layer types combine for final VR output. Passthrough shows real world, projection renders 3D scene, quads/cylinders add UI overlays.
The example uses several compositor layers to render the scene:
- Passthrough Layer: Displays the real-world view from the headset's cameras.
- Projection Layer: Renders the 3D scene (triangles).
- Quad Layer: Renders an ImGui overlay as a flat quad in the 3D space.
- Cylinder Layer: Renders an ImGui overlay as a cylinder around the user.
OpenXR Layers Initialization¶
In OpenXR, we define different layers that the compositor will blend together. ```cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
``` Filename: hello_xr.cpp
OpenXR Actions¶
Actions allow us to handle input from various VR controllers in a device-independent way.
cpp
m_actionSet = m_xrInstance.createActionSet({ .name = "default", .localizedName = "Default" });
m_toggleRotateYAction = m_actionSet.createAction({ .name = "rotatey",
.localizedName = "RotateY",
.type = KDXr::ActionType::BooleanInput,
.subactionPaths = m_handPaths });
m_toggleRotateZAction = m_actionSet.createAction({ .name = "toggle_animation",
.localizedName = "Toggle Animation",
.type = KDXr::ActionType::BooleanInput,
.subactionPaths = m_handPaths });
m_scaleAction = m_actionSet.createAction({ .name = "scale",
.localizedName = "Scale",
.type = KDXr::ActionType::FloatInput,
.subactionPaths = { m_handPaths[0] } });
m_translateAction = m_actionSet.createAction({ .name = "translate",
.localizedName = "Translate",
.type = KDXr::ActionType::Vector2Input,
.subactionPaths = { m_handPaths[0] } });
m_palmPoseAction = m_actionSet.createAction({ .name = "palm_pose",
.localizedName = "Palm Pose",
.type = KDXr::ActionType::PoseInput,
.subactionPaths = m_handPaths });
m_buzzAction = m_actionSet.createAction({ .name = "buzz",
.localizedName = "Buzz",
.type = KDXr::ActionType::VibrationOutput,
.subactionPaths = m_handPaths });
m_togglePassthroughAction = m_actionSet.createAction({ .name = "passthrough",
.localizedName = "Toggle Passthrough",
.type = KDXr::ActionType::BooleanInput,
.subactionPaths = { m_handPaths[1] } });
m_mouseButtonAction = m_actionSet.createAction({ .name = "mousebutton",
.localizedName = "Mouse Button",
.type = KDXr::ActionType::BooleanInput,
.subactionPaths = { m_handPaths[1] } });
// Create action spaces for the palm poses. Default is no offset from the palm pose. If you wish to
// apply an offset, you can do so by setting the poseInActionSpace member of the ActionSpaceOptions.
for (uint32_t i = 0; i < 2; ++i)
m_palmPoseActionSpaces[i] = m_session.createActionSpace({ .action = m_palmPoseAction, .subactionPath = m_handPaths[i] });
// Suggest some bindings for the actions. NB: This assumes we are using a Meta Quest. If you are using a different
// device, you will need to change the suggested bindings.
const auto bindingOptions = KDXr::SuggestActionBindingsOptions{
.interactionProfile = "/interaction_profiles/oculus/touch_controller",
.suggestedBindings = {
{ .action = m_toggleRotateYAction, .binding = "/user/hand/right/input/b/click" },
{ .action = m_toggleRotateYAction, .binding = "/user/hand/left/input/y/click" },
{ .action = m_toggleRotateZAction, .binding = "/user/hand/left/input/x/click" },
{ .action = m_toggleRotateZAction, .binding = "/user/hand/right/input/a/click" },
{ .action = m_scaleAction, .binding = "/user/hand/left/input/trigger/value" },
{ .action = m_translateAction, .binding = "/user/hand/left/input/thumbstick" },
{ .action = m_palmPoseAction, .binding = "/user/hand/left/input/aim/pose" },
{ .action = m_palmPoseAction, .binding = "/user/hand/right/input/aim/pose" },
{ .action = m_buzzAction, .binding = "/user/hand/left/output/haptic" },
{ .action = m_buzzAction, .binding = "/user/hand/right/output/haptic" },
{ .action = m_togglePassthroughAction, .binding = "/user/hand/right/input/thumbstick/click" },
{ .action = m_mouseButtonAction, .binding = "/user/hand/right/input/trigger/value" },
}
};
if (m_xrInstance.suggestActionBindings(bindingOptions) != KDXr::SuggestActionBindingsResult::Success) {
SPDLOG_LOGGER_ERROR(m_logger, "Failed to suggest action bindings.");
}
// Attach the action set to the session
const auto attachOptions = KDXr::AttachActionSetsOptions{ .actionSets = { m_actionSet } };
if (m_session.attachActionSets(attachOptions) != KDXr::AttachActionSetsResult::Success) {
SPDLOG_LOGGER_ERROR(m_logger, "Failed to attach action set.");
}
Filename: hello_xr.cpp
View Matrix Calculation¶
In VR, we need to calculate a separate view and projection matrix for each eye.
1 | |
Filename: projection_layer.cpp
Rendering the Scene¶
The scene is rendered for each eye using the corresponding view and projection matrices.
1 | |
Filename: projection_layer.cpp
Vulkan Integration¶
Vulkan and OpenXR: OpenXR abstracts VR hardware but uses native graphics APIs (Vulkan, D3D, etc.) for rendering. KDGpu's OpenXR integration:
- Creates Vulkan swapchains for each OpenXR swapchain
- Handles per-eye rendering with multiview or separate passes
- Manages frame timing and synchronization with the VR runtime
Key Vulkan Features Used:
- VK_KHR_multiview - Efficient stereo rendering
- Swapchains - Frame buffer management
- Semaphores and Fences - Frame synchronization
Further Reading¶
Updated on 2026-03-31 at 00:02:07 +0000