Use Spatial Containers in Godot

Applicable XR devices
This guidance helps you build experiences for these types of XR devices.
XR Headsets
Wired XR Glasses

Spatial Containers let your app render 3D OpenXR content inside a system-managed 3D volumetric bounding box in Full Space Mode, or transition at runtime into a full-screen unbounded experience.

Follow this guide to configure and use Spatial Containers in your project.

Multiple spatial containers running in Full Space Mode on Android XR.
Figure 1. Spatial containers in Full Space Mode, including a floor-locked 3D terrain container and a 3D model paired with 2D UI.

How Spatial Containers work

When you enable Spatial Container in your project settings:

  • Your entire active 3D scene is placed inside a single primary spatial container centered at the scene origin (0, 0, 0). You don't need to tag or assign individual Node3D nodes to place them inside the container.
  • Your app launches into managed Full Space Mode using the Bounds Mode (Bounded or Immersive) and initial Bounds dimensions configured in your project settings. In bounded mode, the system automatically clips any 3D geometry in your scene that extends outside the bounding box's dimensions.
  • By default, the Android XR system provides window controls so the user can grab, move, and reposition the bounded container in 3D space, or minimize and close it. At runtime, your app can resize the container, switch between bounded mode (OpenXRSpatialContainerState.BOUNDED) and full-screen immersive mode (OpenXRSpatialContainerState.IMMERSIVE), and react to bounds and state changes using the OpenXRSpatialContainerExtension singleton.

Prerequisites

Before beginning, ensure your development environment and target device meet the following requirements:

  • Godot version: Godot 4.8-dev or higher.
  • Project setup: Complete all steps in the Godot Engine project setup guide.
  • Godot OpenXR Vendors Plugin: Install version 5.2 or higher of the Godot OpenXR Vendors Plugin, which enables support for Android XR specific extensions.
  • Target device: An Android XR device supporting Spatial API Level 4 or higher. At runtime, call OpenXRSpatialContainerExtension.is_enabled() to verify whether the extension is available on the device.

Enable Spatial Containers in project settings

Configure your project settings to enable OpenXR, Spatial Containers, automatic runtime permissions, and alpha-blended passthrough:

  1. Open Project > Project Settings.
  2. Enable the Advanced Settings toggle in the top-right corner of the Project Settings window.
  3. Navigate to General > XR > Shaders and select Enabled.
  4. Navigate to General > XR > OpenXR and configure the following settings:
    • Select Enabled to turn on OpenXR for the project.
    • Set Environment Blend Mode to Alpha Blend.
  5. Also under OpenXR, go to the Extensions section and configure the following settings:
    • Select Automatically Request Runtime Permissions so your app automatically prompts the user at startup for any XR permissions declared in your Android manifest.
    • Under the Spatial Container subsection, select Enabled.
    • Set Bounds Mode to Bounded to launch in a 3D bounding box, or Immersive to launch in full-screen immersive mode.
    • Set Bounds to the initial width, height, and depth dimensions in meters, such as Vector3(1.0, 1.0, 1.0).
  6. Navigate to General > Rendering > Viewport and select Transparent Background.

Configure Android export, permissions, and Full Space Mode

Next, configure your Android export preset, permissions, and activity start mode so your app runs in Full Space Mode with Spatial Containers enabled.

Configure the Android export preset

  1. Go to Project > Export... and select your Android export preset.
  2. Under Gradle Build, select Use Gradle Build and set Min SDK to 34 or higher.

    Enable the

  3. Under XR Features, set XR Mode to OpenXR and select Enable AndroidXR Plugin.

    Configure the options in the

  4. Under Android XR Features, select Use Experimental Features.

Add scene understanding permissions for floor locking

Creating and rendering a basic spatial container does not require dangerous runtime permissions (and android.permission.HEAD_TRACKING is added to the manifest automatically when Use Experimental Features is enabled). However, if your app uses scene understanding features such as locking and anchoring a spatial container to the physical floor, your app must declare and request the android.permission.SCENE_UNDERSTANDING_COARSE permission at runtime.

See Understand permissions for XR for details on Android XR permissions. If your app uses floor locking or scene understanding, declare and request android.permission.SCENE_UNDERSTANDING_COARSE:

  1. In Project > Export... > Android, go to Permissions > Custom Permissions and add android.permission.SCENE_UNDERSTANDING_COARSE.

  2. When you select Automatically Request Runtime Permissions in Project Settings > General > XR > OpenXR > Extensions, your app automatically requests the permission when OpenXR starts. If you need to request the permission manually in GDScript instead, call OS.request_permission():

    if not OS.get_granted_permissions().has("android.permission.SCENE_UNDERSTANDING_COARSE"):
        OS.request_permission("android.permission.SCENE_UNDERSTANDING_COARSE")
    

Configure Full Space Mode

Spatial Containers on Android XR only support Full Space Mode (XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED). Make sure your Android activity declares XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED in your custom build template (android/build/AndroidManifest.xml, installed using Project > Install Android Build Template...):

<manifest ...>
    <application ...>
        <property
            android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
            android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED"
            tools:replace="android:value" />
        ...
    </application>
</manifest>

Work with Spatial Containers in GDScript

The OpenXRSpatialContainerExtension singleton and the OpenXRSpatialContainerState class expose the XR_EXT_spatial_container and XR_EXT_spatial_container_self_rendering extensions to GDScript. At runtime, use OpenXRSpatialContainerExtension to inspect the main spatial container's state and bounds, listen for container signals, and request transitions between BOUNDED mode (a 3D bounding box) and IMMERSIVE mode (a full-screen unbounded view).

Connect to Spatial Container signals

Check OpenXRSpatialContainerExtension.is_enabled() to verify that the Spatial Container extension is supported and active on the current device, and connect to container bounds and state signals in _ready().

OpenXRSpatialContainerExtension emits the following state signals for the spatial container:

  • spatial_container_visibility_changed: Emitted with (spatial_container, is_visible) when the container is shown (true) or hidden (false). Pause animations, audio, or simulation updates when is_visible is false.
  • spatial_container_interactability_changed: Emitted with (spatial_container, is_interactive) when the container gains (true) or loses (false) user input focus. Enable or disable interactive highlights and input handling when focus changes.
  • spatial_container_bounds_mode_changed: Emitted with (spatial_container, bounds_mode) when the container switches between OpenXRSpatialContainerState.BOUNDED and OpenXRSpatialContainerState.IMMERSIVE. Toggle environment visuals (such as skyboxes or passthrough) and mode-specific UI controls.
func _ready() -> void:
    if OpenXRSpatialContainerExtension.is_enabled():
        OpenXRSpatialContainerExtension.spatial_container_bounds_changed.connect(
            _on_bounds_changed
        )
        OpenXRSpatialContainerExtension.spatial_container_bounds_mode_changed.connect(
            _on_bounds_mode_changed
        )
        OpenXRSpatialContainerExtension.spatial_container_visibility_changed.connect(
            _on_visibility_changed
        )
        OpenXRSpatialContainerExtension.spatial_container_interactability_changed.connect(
            _on_interactability_changed
        )

func _on_visibility_changed(_rid: RID, is_visible: bool) -> void:
    # Pause or resume simulation and audio based on visibility
    pass

func _on_interactability_changed(_rid: RID, is_interactive: bool) -> void:
    # Enable or disable input handling and focus highlights
    pass

Scale 3D content to container bounds

When in BOUNDED mode, scale your 3D content uniformly by the smallest dimension of bounds so that it preserves its aspect ratio and always fits inside the smallest side of the container. When transitioning to IMMERSIVE mode, restore your scene to its 1:1 scale:

func _on_bounds_changed(_rid: RID, bounds: Vector3) -> void:
    var uniform_scale: float = min(bounds.x, bounds.y, bounds.z)
    content_root.scale = Vector3.ONE * uniform_scale

func _on_bounds_mode_changed(
    _rid: RID, bounds_mode: OpenXRSpatialContainerState.BoundsMode
) -> void:
    if bounds_mode == OpenXRSpatialContainerState.IMMERSIVE:
        content_root.scale = Vector3.ONE

Switch between bounded and immersive modes

Call request_main_spatial_container_bounds_mode with OpenXRSpatialContainerState.BOUNDED or OpenXRSpatialContainerState.IMMERSIVE to switch between a bounded 3D volume and a full-screen immersive view:

# Request transitioning to immersive mode (or pass BOUNDED to return)
OpenXRSpatialContainerExtension.request_main_spatial_container_bounds_mode(
    OpenXRSpatialContainerState.IMMERSIVE
)

See also


OpenXR™ and the OpenXR logo are trademarks owned by The Khronos Group Inc. and are registered as a trademark in China, the European Union, Japan and the United Kingdom.