Skip to content

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
const XrPassthroughLayerOptions passthroughLayerOptions = {
    .device = &m_device,
    .queue = &m_queue,
    .session = &m_session
};
m_passthroughLayer = createCompositorLayer<XrPassthroughLayer>(passthroughLayerOptions);

// Create a projection layer to render the 3D scene
const XrProjectionLayerOptions projectionLayerOptions = {
    .device = &m_device,
    .queue = &m_queue,
    .session = &m_session,
    .colorSwapchainFormat = m_colorSwapchainFormat,
    .depthSwapchainFormat = m_depthSwapchainFormat,
    .samples = m_samples.get(),
    .requestMultiview = false
};
m_projectionLayer = createCompositorLayer<ProjectionLayer>(projectionLayerOptions);
m_projectionLayer->setReferenceSpace(m_referenceSpace);

// Create a quad layer to render the ImGui overlay
const XrQuadLayerOptions quadLayerOptions = {
    .device = &m_device,
    .queue = &m_queue,
    .session = &m_session,
    .colorSwapchainFormat = m_colorSwapchainFormat,
    .depthSwapchainFormat = m_depthSwapchainFormat,
    .samples = m_samples.get()
};
m_quadImguiLayer = createCompositorLayer<XrQuadImGuiLayer>(quadLayerOptions);
m_quadImguiLayer->setReferenceSpace(m_referenceSpace);
m_quadImguiLayer->position = { -1.0f, 0.2f, -1.5f };

// Create a cylinder layer to render the ImGui overlay
const XrCylinderLayerOptions cylinderLayerOptions = {
    .device = &m_device,
    .queue = &m_queue,
    .session = &m_session,
    .colorSwapchainFormat = m_colorSwapchainFormat,
    .depthSwapchainFormat = m_depthSwapchainFormat,
    .samples = m_samples.get()
};
m_cylinderImguiLayer = createCompositorLayer<XrCylinderImGuiLayer>(cylinderLayerOptions);
m_cylinderImguiLayer->setReferenceSpace(m_referenceSpace);
m_cylinderImguiLayer->radius = 2.0f;
m_cylinderImguiLayer->position = { m_cylinderImguiLayer->radius() / 2.0f, 0.2f, 0.0f };
m_cylinderImguiLayer->centralAngle = 1.0f; // 1 radians = 57.3 degrees

m_cylinderImguiLayer->registerImGuiOverlayDrawFunction([this](ImGuiContext *ctx) {
    ImGui::SetCurrentContext(ctx);
    drawEditCylinderUi();
});

``` 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:

Further Reading


Updated on 2026-03-31 at 00:02:07 +0000