فهرست منبع

Merge pull request #107388 from BastiaanOlij/openxr_render_models_ext

OpenXR: Add support for render models extension
Thaddeus Crews 1 ماه پیش
والد
کامیت
c7d2ea4f68

+ 4 - 0
doc/classes/ProjectSettings.xml

@@ -3451,6 +3451,10 @@
 			If [code]true[/code], support for the unobstructed data source is requested. If supported, you will receive hand tracking data based on the actual finger positions of the user often determined by optical tracking.
 			[b]Note:[/b] This requires the OpenXR data source extension and unobstructed handtracking to be supported by the XR runtime. If not supported this setting will be ignored. [member xr/openxr/extensions/hand_tracking] must be enabled for this setting to be used.
 		</member>
+		<member name="xr/openxr/extensions/render_model" type="bool" setter="" getter="" default="false">
+			If [code]true[/code] we enable the render model extension if available.
+			[b]Note:[/b] This relates to the core OpenXR render model extension and has no relation to any vendor render model extensions.
+		</member>
 		<member name="xr/openxr/form_factor" type="int" setter="" getter="" default="&quot;0&quot;">
 			Specify whether OpenXR should be configured for an HMD or a hand held device.
 		</member>

+ 1 - 0
main/main.cpp

@@ -2773,6 +2773,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 	GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking_controller_data_source", false); // XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT
 	GLOBAL_DEF_RST_BASIC("xr/openxr/extensions/hand_interaction_profile", false);
 	GLOBAL_DEF_RST_BASIC("xr/openxr/extensions/eye_gaze_interaction", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/render_model", false);
 
 	// OpenXR Binding modifier settings
 	GLOBAL_DEF_BASIC("xr/openxr/binding_modifiers/analog_threshold", false);

+ 3 - 0
modules/openxr/config.py

@@ -40,6 +40,9 @@ def get_doc_classes():
         "OpenXRBindingModifierEditor",
         "OpenXRHapticBase",
         "OpenXRHapticVibration",
+        "OpenXRRenderModelExtension",
+        "OpenXRRenderModel",
+        "OpenXRRenderModelManager",
     ]
 
 

+ 6 - 0
modules/openxr/doc_classes/OpenXRExtensionWrapper.xml

@@ -185,6 +185,12 @@
 				Called when the OpenXR session state is changed to visible. This means OpenXR is now ready to receive frames.
 			</description>
 		</method>
+		<method name="_on_sync_actions" qualifiers="virtual">
+			<return type="void" />
+			<description>
+				Called when OpenXR has performed its action sync.
+			</description>
+		</method>
 		<method name="_on_viewport_composition_layer_destroyed" qualifiers="virtual">
 			<return type="void" />
 			<param index="0" name="layer" type="const void*" />

+ 32 - 0
modules/openxr/doc_classes/OpenXRRenderModel.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRRenderModel" inherits="Node3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		This node will display an OpenXR render model.
+	</brief_description>
+	<description>
+		This node will display an OpenXR render model by accessing the associated GLTF and processes all animation data (if supported by the XR runtime).
+		Render models were introduced to allow showing the correct model for the controller (or other device) the user has in hand, since the OpenXR action map does not provide information about the hardware used by the user. Note that while the controller (or device) can be somewhat inferred by the bound action map profile, this is a dangerous approach as the user may be using hardware not known at time of development and OpenXR will simply simulate an available interaction profile.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_top_level_path" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the top level path related to this render model.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="render_model" type="RID" setter="set_render_model" getter="get_render_model" default="RID()">
+			The render model RID for the render model to load, as returned by [method OpenXRRenderModelExtension.render_model_create] or [method OpenXRRenderModelExtension.render_model_get_all].
+		</member>
+	</members>
+	<signals>
+		<signal name="render_model_top_level_path_changed">
+			<description>
+				Emitted when the top level path of this render model has changed.
+			</description>
+		</signal>
+	</signals>
+</class>

+ 129 - 0
modules/openxr/doc_classes/OpenXRRenderModelExtension.xml

@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRRenderModelExtension" inherits="OpenXRExtensionWrapper" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		This class implements the OpenXR Render Model Extension.
+	</brief_description>
+	<description>
+		This class implements the OpenXR Render Model Extension, if enabled it will maintain a list of active render models and provides an interface to the render model data.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="is_active" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if OpenXR's render model extension is supported and enabled.
+				[b]Note:[/b] This only returns a valid value after OpenXR has been initialized.
+			</description>
+		</method>
+		<method name="render_model_create">
+			<return type="RID" />
+			<param index="0" name="render_model_id" type="int" />
+			<description>
+				Creates a render model object within OpenXR using a render model id.
+				[b]Note:[/b] This function is exposed for dependent OpenXR extensions that provide render model ids to be used with the render model extension.
+			</description>
+		</method>
+		<method name="render_model_destroy">
+			<return type="void" />
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Destroys a render model object within OpenXR that was previously created with [method render_model_create].
+				[b]Note:[/b] This function is exposed for dependent OpenXR extensions that provide render model ids to be used with the render model extension.
+			</description>
+		</method>
+		<method name="render_model_get_all">
+			<return type="RID[]" />
+			<description>
+				Returns an array of all currently active render models registered with this extension.
+			</description>
+		</method>
+		<method name="render_model_get_animatable_node_count" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Returns the number of animatable nodes this render model has.
+			</description>
+		</method>
+		<method name="render_model_get_animatable_node_name" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="render_model" type="RID" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Returns the name of the given animatable node.
+			</description>
+		</method>
+		<method name="render_model_get_animatable_node_transform" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="render_model" type="RID" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Returns the current local transform for an animatable node. This is updated every frame.
+			</description>
+		</method>
+		<method name="render_model_get_confidence" qualifiers="const">
+			<return type="int" enum="XRPose.TrackingConfidence" />
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Returns the tracking confidence of the tracking data for the render model.
+			</description>
+		</method>
+		<method name="render_model_get_root_transform" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Returns the root transform of a render model. This is the tracked position relative to our [XROrigin3D] node.
+			</description>
+		</method>
+		<method name="render_model_get_subaction_paths">
+			<return type="PackedStringArray" />
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Returns a list of active subaction paths for this [param render_model].
+				[b]Note:[/b] If different devices are bound to your actions than available in suggested interaction bindings, this information shows paths related to the interaction bindings being mimicked by that device.
+			</description>
+		</method>
+		<method name="render_model_get_top_level_path" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Returns the top level path associated with this [param render_model]. If provided this identifies whether the render model is associated with the players hands or other body part.
+			</description>
+		</method>
+		<method name="render_model_is_animatable_node_visible" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="render_model" type="RID" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Returns [code]true[/code] if this animatable node should be visible.
+			</description>
+		</method>
+		<method name="render_model_new_scene_instance" qualifiers="const">
+			<return type="Node3D" />
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Returns an instance of a subscene that contains all [MeshInstance3D] nodes that allow you to visualize the render model.
+			</description>
+		</method>
+	</methods>
+	<signals>
+		<signal name="render_model_added">
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Emitted when a new render model is added.
+			</description>
+		</signal>
+		<signal name="render_model_removed">
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Emitted when a render model is removed.
+			</description>
+		</signal>
+		<signal name="render_model_top_level_path_changed">
+			<param index="0" name="render_model" type="RID" />
+			<description>
+				Emitted when the top level path associated with a render model changed.
+			</description>
+		</signal>
+	</signals>
+</class>

+ 48 - 0
modules/openxr/doc_classes/OpenXRRenderModelManager.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRRenderModelManager" inherits="Node3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Helper node that will automatically manage displaying render models.
+	</brief_description>
+	<description>
+		This helper node will automatically manage displaying render models. It will create new [OpenXRRenderModel] nodes as controllers and other hand held devices are detected, and remove those nodes when they are deactivated.
+		[b]Note:[/b] If you want more control over this logic you can alternatively call [method OpenXRRenderModelExtension.render_model_get_all] to obtain a list of active render model ids and create [OpenXRRenderModel] instances for each render model id provided.
+	</description>
+	<tutorials>
+	</tutorials>
+	<members>
+		<member name="make_local_to_pose" type="String" setter="set_make_local_to_pose" getter="get_make_local_to_pose" default="&quot;&quot;">
+			Position render models local to this pose (this will adjust the position of the render models container node).
+		</member>
+		<member name="tracker" type="int" setter="set_tracker" getter="get_tracker" enum="OpenXRRenderModelManager.RenderModelTracker" default="0">
+			Limits render models to the specified tracker. Include: 0 = All render models, 1 = Render models not related to a tracker, 2 = Render models related to the left hand tracker, 3 = Render models related to the right hand tracker.
+		</member>
+	</members>
+	<signals>
+		<signal name="render_model_added">
+			<param index="0" name="render_model" type="OpenXRRenderModel" />
+			<description>
+				Emitted when a render model node is added as a child to this node.
+			</description>
+		</signal>
+		<signal name="render_model_removed">
+			<param index="0" name="render_model" type="OpenXRRenderModel" />
+			<description>
+				Emitted when a render model child node is about to be removed from this node.
+			</description>
+		</signal>
+	</signals>
+	<constants>
+		<constant name="RENDER_MODEL_TRACKER_ANY" value="0" enum="RenderModelTracker">
+			All active render models are shown regardless of what tracker they relate to.
+		</constant>
+		<constant name="RENDER_MODEL_TRACKER_NONE_SET" value="1" enum="RenderModelTracker">
+			Only active render models are shown that are not related to any tracker we manage.
+		</constant>
+		<constant name="RENDER_MODEL_TRACKER_LEFT_HAND" value="2" enum="RenderModelTracker">
+			Only active render models are shown that are related to the left hand tracker.
+		</constant>
+		<constant name="RENDER_MODEL_TRACKER_RIGHT_HAND" value="3" enum="RenderModelTracker">
+			Only active render models are shown that are related to the right hand tracker.
+		</constant>
+	</constants>
+</class>

+ 5 - 0
modules/openxr/extensions/openxr_extension_wrapper.cpp

@@ -55,6 +55,7 @@ void OpenXRExtensionWrapper::_bind_methods() {
 	GDVIRTUAL_BIND(_on_instance_destroyed);
 	GDVIRTUAL_BIND(_on_session_created, "session");
 	GDVIRTUAL_BIND(_on_process);
+	GDVIRTUAL_BIND(_on_sync_actions);
 	GDVIRTUAL_BIND(_on_pre_render);
 	GDVIRTUAL_BIND(_on_main_swapchains_created);
 	GDVIRTUAL_BIND(_on_pre_draw_viewport, "viewport");
@@ -252,6 +253,10 @@ void OpenXRExtensionWrapper::on_process() {
 	GDVIRTUAL_CALL(_on_process);
 }
 
+void OpenXRExtensionWrapper::on_sync_actions() {
+	GDVIRTUAL_CALL(_on_sync_actions);
+}
+
 void OpenXRExtensionWrapper::on_pre_render() {
 	GDVIRTUAL_CALL(_on_pre_render);
 }

+ 2 - 0
modules/openxr/extensions/openxr_extension_wrapper.h

@@ -120,6 +120,7 @@ public:
 	// this happens right before physics process and normal processing is run.
 	// This is when controller data is queried and made available to game logic.
 	virtual void on_process();
+	virtual void on_sync_actions(); // `on_sync_actions` is called right after we sync our action sets.
 	virtual void on_pre_render(); // `on_pre_render` is called right before we start rendering our XR viewports.
 	virtual void on_main_swapchains_created(); // `on_main_swapchains_created` is called right after our main swapchains are (re)created.
 	virtual void on_pre_draw_viewport(RID p_render_target); // `on_pre_draw_viewport` is called right before we start rendering this viewport
@@ -131,6 +132,7 @@ public:
 	GDVIRTUAL0(_on_instance_destroyed);
 	GDVIRTUAL1(_on_session_created, uint64_t);
 	GDVIRTUAL0(_on_process);
+	GDVIRTUAL0(_on_sync_actions);
 	GDVIRTUAL0(_on_pre_render);
 	GDVIRTUAL0(_on_main_swapchains_created);
 	GDVIRTUAL0(_on_session_destroyed);

+ 794 - 0
modules/openxr/extensions/openxr_render_model_extension.cpp

@@ -0,0 +1,794 @@
+/**************************************************************************/
+/*  openxr_render_model_extension.cpp                                     */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "openxr_render_model_extension.h"
+
+#include "../openxr_api.h"
+#include "../openxr_interface.h"
+
+#include "core/config/project_settings.h"
+#include "core/string/print_string.h"
+#include "servers/xr_server.h"
+
+OpenXRRenderModelExtension *OpenXRRenderModelExtension::singleton = nullptr;
+
+OpenXRRenderModelExtension *OpenXRRenderModelExtension::get_singleton() {
+	return singleton;
+}
+
+void OpenXRRenderModelExtension::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("is_active"), &OpenXRRenderModelExtension::is_active);
+	ClassDB::bind_method(D_METHOD("render_model_create", "render_model_id"), &OpenXRRenderModelExtension::render_model_create);
+	ClassDB::bind_method(D_METHOD("render_model_destroy", "render_model"), &OpenXRRenderModelExtension::render_model_destroy);
+	ClassDB::bind_method(D_METHOD("render_model_get_all"), &OpenXRRenderModelExtension::render_model_get_all);
+	ClassDB::bind_method(D_METHOD("render_model_new_scene_instance", "render_model"), &OpenXRRenderModelExtension::render_model_new_scene_instance);
+	ClassDB::bind_method(D_METHOD("render_model_get_subaction_paths", "render_model"), &OpenXRRenderModelExtension::render_model_get_subaction_paths);
+	ClassDB::bind_method(D_METHOD("render_model_get_top_level_path", "render_model"), &OpenXRRenderModelExtension::render_model_get_top_level_path_as_string);
+	ClassDB::bind_method(D_METHOD("render_model_get_confidence", "render_model"), &OpenXRRenderModelExtension::render_model_get_confidence);
+	ClassDB::bind_method(D_METHOD("render_model_get_root_transform", "render_model"), &OpenXRRenderModelExtension::render_model_get_root_transform);
+	ClassDB::bind_method(D_METHOD("render_model_get_animatable_node_count", "render_model"), &OpenXRRenderModelExtension::render_model_get_animatable_node_count);
+	ClassDB::bind_method(D_METHOD("render_model_get_animatable_node_name", "render_model", "index"), &OpenXRRenderModelExtension::render_model_get_animatable_node_name);
+	ClassDB::bind_method(D_METHOD("render_model_is_animatable_node_visible", "render_model", "index"), &OpenXRRenderModelExtension::render_model_is_animatable_node_visible);
+	ClassDB::bind_method(D_METHOD("render_model_get_animatable_node_transform", "render_model", "index"), &OpenXRRenderModelExtension::render_model_get_animatable_node_transform);
+
+	ADD_SIGNAL(MethodInfo("render_model_added", PropertyInfo(Variant::RID, "render_model")));
+	ADD_SIGNAL(MethodInfo("render_model_removed", PropertyInfo(Variant::RID, "render_model")));
+	ADD_SIGNAL(MethodInfo("render_model_top_level_path_changed", PropertyInfo(Variant::RID, "render_model")));
+}
+
+OpenXRRenderModelExtension::OpenXRRenderModelExtension() {
+	singleton = this;
+}
+
+OpenXRRenderModelExtension::~OpenXRRenderModelExtension() {
+	singleton = nullptr;
+}
+
+HashMap<String, bool *> OpenXRRenderModelExtension::get_requested_extensions() {
+	HashMap<String, bool *> request_extensions;
+
+	if (GLOBAL_GET("xr/openxr/extensions/render_model")) {
+		request_extensions[XR_EXT_UUID_EXTENSION_NAME] = &uuid_ext;
+		request_extensions[XR_EXT_RENDER_MODEL_EXTENSION_NAME] = &render_model_ext;
+		request_extensions[XR_EXT_INTERACTION_RENDER_MODEL_EXTENSION_NAME] = &interaction_render_model_ext;
+	}
+
+	return request_extensions;
+}
+
+void OpenXRRenderModelExtension::on_instance_created(const XrInstance p_instance) {
+	// Standard entry points we use.
+	EXT_INIT_XR_FUNC(xrLocateSpace);
+	EXT_INIT_XR_FUNC(xrDestroySpace);
+	EXT_INIT_XR_FUNC(xrPathToString);
+
+	if (render_model_ext) {
+		EXT_INIT_XR_FUNC(xrCreateRenderModelEXT);
+		EXT_INIT_XR_FUNC(xrDestroyRenderModelEXT);
+		EXT_INIT_XR_FUNC(xrGetRenderModelPropertiesEXT);
+		EXT_INIT_XR_FUNC(xrCreateRenderModelSpaceEXT);
+		EXT_INIT_XR_FUNC(xrCreateRenderModelAssetEXT);
+		EXT_INIT_XR_FUNC(xrDestroyRenderModelAssetEXT);
+		EXT_INIT_XR_FUNC(xrGetRenderModelAssetDataEXT);
+		EXT_INIT_XR_FUNC(xrGetRenderModelAssetPropertiesEXT);
+		EXT_INIT_XR_FUNC(xrGetRenderModelStateEXT);
+	}
+
+	if (interaction_render_model_ext) {
+		EXT_INIT_XR_FUNC(xrEnumerateInteractionRenderModelIdsEXT);
+		EXT_INIT_XR_FUNC(xrEnumerateRenderModelSubactionPathsEXT);
+		EXT_INIT_XR_FUNC(xrGetRenderModelPoseTopLevelUserPathEXT);
+	}
+}
+
+void OpenXRRenderModelExtension::on_session_created(const XrSession p_session) {
+	_interaction_data_dirty = true;
+}
+
+void OpenXRRenderModelExtension::on_instance_destroyed() {
+	xrCreateRenderModelEXT_ptr = nullptr;
+	xrDestroyRenderModelEXT_ptr = nullptr;
+	xrGetRenderModelPropertiesEXT_ptr = nullptr;
+	xrCreateRenderModelSpaceEXT_ptr = nullptr;
+	xrCreateRenderModelAssetEXT_ptr = nullptr;
+	xrDestroyRenderModelAssetEXT_ptr = nullptr;
+	xrGetRenderModelAssetDataEXT_ptr = nullptr;
+	xrGetRenderModelAssetPropertiesEXT_ptr = nullptr;
+	xrGetRenderModelStateEXT_ptr = nullptr;
+	xrEnumerateInteractionRenderModelIdsEXT_ptr = nullptr;
+	xrEnumerateRenderModelSubactionPathsEXT_ptr = nullptr;
+	xrGetRenderModelPoseTopLevelUserPathEXT_ptr = nullptr;
+
+	uuid_ext = false;
+	render_model_ext = false;
+	interaction_render_model_ext = false;
+}
+
+void OpenXRRenderModelExtension::on_session_destroyed() {
+	_clear_interaction_data();
+	_clear_render_model_data();
+
+	// We no longer have valid sync data.
+	xr_sync_has_run = false;
+}
+
+bool OpenXRRenderModelExtension::on_event_polled(const XrEventDataBuffer &event) {
+	if (event.type == XR_TYPE_EVENT_DATA_INTERACTION_RENDER_MODELS_CHANGED_EXT) {
+		// Mark interaction data as dirty so that we update it on sync.
+		_interaction_data_dirty = true;
+
+		return true;
+	} else if (event.type == XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED) {
+		// If our controller bindings changed, its likely our render models change too.
+		// We should be getting a XR_TYPE_EVENT_DATA_INTERACTION_RENDER_MODELS_CHANGED_EXT
+		// but checking for this scenario just in case.
+		_interaction_data_dirty = true;
+
+		// Do not consider this handled, we simply do additional logic.
+		return false;
+	}
+
+	return false;
+}
+
+void OpenXRRenderModelExtension::on_sync_actions() {
+	if (!is_active()) {
+		return;
+	}
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	// Mark sync as run
+	xr_sync_has_run = true;
+
+	// Update our interaction data if needed
+	if (_interaction_data_dirty) {
+		_update_interaction_data();
+	}
+
+	// Loop through all of our render models to update our space and state info
+	LocalVector<RID> owned = render_model_owner.get_owned_list();
+
+	for (const RID &rid : owned) {
+		RenderModel *render_model = render_model_owner.get_or_null(rid);
+		if (render_model && render_model->xr_space != XR_NULL_HANDLE) {
+			XrSpaceLocation render_model_location = {
+				XR_TYPE_SPACE_LOCATION, // type
+				nullptr, // next
+				0, // locationFlags
+				{ { 0.0, 0.0, 0.0, 1.0 }, { 0.0, 0.0, 0.0 } }, // pose
+			};
+
+			XrResult result = xrLocateSpace(render_model->xr_space, openxr_api->get_play_space(), openxr_api->get_predicted_display_time(), &render_model_location);
+			ERR_CONTINUE_MSG(XR_FAILED(result), "OpenXR: Failed to locate render model space [" + openxr_api->get_error_string(result) + "]");
+
+			render_model->confidence = openxr_api->transform_from_location(render_model_location, render_model->root_transform);
+
+			if (!render_model->node_states.is_empty()) {
+				// Get node states.
+				XrRenderModelStateGetInfoEXT get_state_info = {
+					XR_TYPE_RENDER_MODEL_STATE_GET_INFO_EXT, // type
+					nullptr, // next
+					openxr_api->get_predicted_display_time() // displayTime
+				};
+
+				XrRenderModelStateEXT state = {
+					XR_TYPE_RENDER_MODEL_STATE_EXT, // type
+					nullptr, // next
+					render_model->animatable_node_count, // nodeStateCount
+					render_model->node_states.ptr(), // nodeStates
+				};
+
+				result = xrGetRenderModelStateEXT(render_model->xr_render_model, &get_state_info, &state);
+				if (XR_FAILED(result)) {
+					ERR_PRINT("OpenXR: Failed to update node states [" + openxr_api->get_error_string(result) + "]");
+				}
+			}
+
+			XrPath new_path = XR_NULL_PATH;
+
+			if (toplevel_paths.is_empty()) {
+				// Set this up just once with paths we support here.
+				toplevel_paths.push_back(openxr_api->get_xr_path("/user/hand/left"));
+				toplevel_paths.push_back(openxr_api->get_xr_path("/user/hand/right"));
+			}
+
+			XrInteractionRenderModelTopLevelUserPathGetInfoEXT info = {
+				XR_TYPE_INTERACTION_RENDER_MODEL_TOP_LEVEL_USER_PATH_GET_INFO_EXT, // type
+				nullptr, // next
+				(uint32_t)toplevel_paths.size(), // topLevelUserPathCount
+				toplevel_paths.ptr() // topLevelUserPaths
+			};
+			result = xrGetRenderModelPoseTopLevelUserPathEXT(render_model->xr_render_model, &info, &new_path);
+			if (XR_FAILED(result)) {
+				ERR_PRINT("OpenXR: Failed to update the top level path for render models [" + openxr_api->get_error_string(result) + "]");
+			} else if (new_path != render_model->top_level_path) {
+				print_verbose("OpenXR: Render model top level path changed to " + openxr_api->get_xr_path_name(new_path));
+
+				// Set the new path
+				render_model->top_level_path = new_path;
+
+				// And broadcast it
+				// Note, converting an XrPath to a String has overhead, so we won't do this automatically.
+				emit_signal(SNAME("render_model_top_level_path_changed"), rid);
+			}
+		}
+	}
+}
+
+bool OpenXRRenderModelExtension::is_active() const {
+	return render_model_ext && interaction_render_model_ext;
+}
+
+void OpenXRRenderModelExtension::_clear_interaction_data() {
+	for (const KeyValue<XrRenderModelIdEXT, RID> &e : interaction_render_models) {
+		render_model_destroy(e.value);
+	}
+	interaction_render_models.clear();
+}
+
+bool OpenXRRenderModelExtension::_update_interaction_data() {
+	ERR_FAIL_COND_V_MSG(!interaction_render_model_ext, false, "Interaction render model extension hasn't been enabled.");
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, false);
+
+	XrSession session = openxr_api->get_session();
+	ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false);
+
+	// Check if syncActions has been run at least once or there is no point in getting data.
+	if (!xr_sync_has_run) {
+		// Do not treat this as an error.
+		return true;
+	}
+
+	// If we get this far, no longer mark as dirty.
+	// Else we just repeat the same error over and over again.
+	_interaction_data_dirty = false;
+
+	// Obtain interaction info.
+	XrInteractionRenderModelIdsEnumerateInfoEXT interaction_info = {
+		XR_TYPE_INTERACTION_RENDER_MODEL_IDS_ENUMERATE_INFO_EXT, // type
+		nullptr, // next
+	};
+
+	// Obtain count.
+	uint32_t interaction_count = 0;
+	XrResult result = xrEnumerateInteractionRenderModelIdsEXT(session, &interaction_info, 0, &interaction_count, nullptr);
+	if (XR_FAILED(result)) {
+		// not successful? then we do nothing.
+		ERR_FAIL_V_MSG(false, "OpenXR: Failed to obtain render model interaction id count [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	// Create some storage
+	LocalVector<XrRenderModelIdEXT> render_model_interaction_ids;
+	render_model_interaction_ids.resize(interaction_count);
+
+	// Only need to fetch data if there is something to fetch (/we've got storage).
+	if (!render_model_interaction_ids.is_empty()) {
+		// Obtain interaction ids
+		result = xrEnumerateInteractionRenderModelIdsEXT(session, &interaction_info, render_model_interaction_ids.size(), &interaction_count, render_model_interaction_ids.ptr());
+		if (XR_FAILED(result)) {
+			ERR_FAIL_V_MSG(false, "OpenXR: Failed to obtain render model interaction ids [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+		}
+	}
+
+	// Remove render models that are no longer tracked
+	LocalVector<XrRenderModelIdEXT> erase_ids;
+	for (const KeyValue<XrRenderModelIdEXT, RID> &e : interaction_render_models) {
+		if (!render_model_interaction_ids.has(e.key)) {
+			if (e.value.is_valid()) {
+				render_model_destroy(e.value);
+			}
+
+			erase_ids.push_back(e.key);
+		}
+	}
+
+	// Remove these from our hashmap
+	for (const XrRenderModelIdEXT &id : erase_ids) {
+		interaction_render_models.erase(id);
+	}
+
+	// Now update our models
+	for (const XrRenderModelIdEXT &id : render_model_interaction_ids) {
+		if (!interaction_render_models.has(id)) {
+			// Even if this fails we add it so we don't repeat trying to create it
+			interaction_render_models[id] = render_model_create(id);
+		}
+	}
+
+	return true;
+}
+
+bool OpenXRRenderModelExtension::has_render_model(RID p_render_model) const {
+	return render_model_owner.owns(p_render_model);
+}
+
+RID OpenXRRenderModelExtension::render_model_create(XrRenderModelIdEXT p_render_model_id) {
+	ERR_FAIL_COND_V_MSG(!render_model_ext, RID(), "Render model extension hasn't been enabled.");
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, RID());
+
+	XrSession session = openxr_api->get_session();
+	ERR_FAIL_COND_V(session == XR_NULL_HANDLE, RID());
+
+	RenderModel render_model;
+	render_model.xr_render_model_id = p_render_model_id;
+
+	// Start with the extensions that are supported in our base (see GLTFDocument::_parse_gltf_extensions).
+	Vector<const char *> supported_gltf_extensions = {
+		"KHR_lights_punctual",
+		"KHR_materials_pbrSpecularGlossiness",
+		"KHR_texture_transform",
+		"KHR_materials_unlit",
+		"KHR_materials_emissive_strength",
+	};
+
+	// Now find anything we support through plugins, which is a bit of a pain as they are converted to Strings
+	// and we need to convert them back.
+	Vector<CharString> char_extensions; // Just for temp storage of our c-strings.
+	Vector<Ref<GLTFDocumentExtension>> gltf_document_extensions = GLTFDocument::get_all_gltf_document_extensions();
+	for (Ref<GLTFDocumentExtension> &gltf_document_extension : gltf_document_extensions) {
+		Vector<String> supported_extensions = gltf_document_extension->get_supported_extensions();
+		for (const String &extension : supported_extensions) {
+			char_extensions.push_back(extension.utf8());
+		}
+	}
+
+	// Now we can add them to our supported extensions list.
+	for (const CharString &cs : char_extensions) {
+		supported_gltf_extensions.push_back(cs.get_data());
+	}
+
+	XrRenderModelCreateInfoEXT create_info = {
+		XR_TYPE_RENDER_MODEL_CREATE_INFO_EXT, // type
+		nullptr, // next
+		p_render_model_id, // renderModelId
+		uint32_t(supported_gltf_extensions.size()), // gltfExtensionCount
+		supported_gltf_extensions.ptr(), // gltfExtensions
+	};
+
+	XrResult result = xrCreateRenderModelEXT(session, &create_info, &render_model.xr_render_model);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(RID(), "OpenXR: Failed to create render model [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	XrRenderModelPropertiesGetInfoEXT properties_info = {
+		XR_TYPE_RENDER_MODEL_PROPERTIES_GET_INFO_EXT, // type
+		nullptr, // next
+	};
+
+	XrRenderModelPropertiesEXT properties;
+	result = xrGetRenderModelPropertiesEXT(render_model.xr_render_model, &properties_info, &properties);
+	if (XR_FAILED(result)) {
+		ERR_PRINT("OpenXR: Failed to get render model properties [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	} else {
+		render_model.animatable_node_count = properties.animatableNodeCount;
+		render_model.render_model_data = _get_render_model_data(properties.cacheId, properties.animatableNodeCount);
+	}
+
+	// Create space for positioning our asset.
+	XrRenderModelSpaceCreateInfoEXT space_create_info = {
+		XR_TYPE_RENDER_MODEL_SPACE_CREATE_INFO_EXT, // type
+		nullptr, // next
+		render_model.xr_render_model // renderModel
+	};
+
+	result = xrCreateRenderModelSpaceEXT(session, &space_create_info, &render_model.xr_space);
+	if (XR_FAILED(result)) {
+		ERR_PRINT("OpenXR: Failed to create render model space [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	if (render_model.animatable_node_count > 0) {
+		render_model.node_states.resize(render_model.animatable_node_count);
+	}
+
+	RID new_rid = render_model_owner.make_rid(render_model);
+
+	emit_signal(SNAME("render_model_added"), new_rid);
+
+	return new_rid;
+}
+
+RID OpenXRRenderModelExtension::_render_model_create(uint64_t p_render_model_id) {
+	RID ret;
+
+	ERR_FAIL_COND_V(p_render_model_id == XR_NULL_RENDER_MODEL_ID_EXT, ret);
+
+	if (is_active()) {
+		ret = render_model_create(XrRenderModelIdEXT(p_render_model_id));
+	}
+
+	return ret;
+}
+
+void OpenXRRenderModelExtension::render_model_destroy(RID p_render_model) {
+	ERR_FAIL_COND_MSG(!render_model_ext, "Render model extension hasn't been enabled.");
+
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL(render_model);
+
+	emit_signal(SNAME("render_model_removed"), p_render_model);
+
+	// Clean up.
+	if (render_model->xr_space != XR_NULL_HANDLE) {
+		xrDestroySpace(render_model->xr_space);
+	}
+
+	render_model->node_states.clear();
+
+	// And destroy our model.
+	XrResult result = xrDestroyRenderModelEXT(render_model->xr_render_model);
+	if (XR_FAILED(result)) {
+		ERR_PRINT("OpenXR: Failed to destroy render model [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	render_model_owner.free(p_render_model);
+}
+
+TypedArray<RID> OpenXRRenderModelExtension::render_model_get_all() {
+	TypedArray<RID> ret;
+
+	LocalVector<RID> rids = render_model_owner.get_owned_list();
+
+	for (const RID &rid : rids) {
+		ret.push_back(rid);
+	}
+
+	return ret;
+}
+
+Node3D *OpenXRRenderModelExtension::render_model_new_scene_instance(RID p_render_model) const {
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, nullptr);
+
+	if (render_model->render_model_data.is_null()) {
+		// We never loaded it (don't spam errors here).
+		return nullptr;
+	}
+
+	return render_model->render_model_data->new_scene_instance();
+}
+
+PackedStringArray OpenXRRenderModelExtension::render_model_get_subaction_paths(RID p_render_model) {
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, PackedStringArray());
+
+	XrInstance instance = openxr_api->get_instance();
+	ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, PackedStringArray());
+
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, PackedStringArray());
+
+	PackedStringArray subaction_paths;
+
+	XrInteractionRenderModelSubactionPathInfoEXT subaction_info = {
+		XR_TYPE_INTERACTION_RENDER_MODEL_SUBACTION_PATH_INFO_EXT, // type
+		nullptr, // next
+	};
+
+	uint32_t capacity;
+
+	XrResult result = xrEnumerateRenderModelSubactionPathsEXT(render_model->xr_render_model, &subaction_info, 0, &capacity, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(PackedStringArray(), "OpenXR: Failed to obtain render model subaction path count [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	if (capacity > 0) {
+		LocalVector<XrPath> paths;
+
+		paths.resize(capacity);
+
+		result = xrEnumerateRenderModelSubactionPathsEXT(render_model->xr_render_model, &subaction_info, capacity, &capacity, paths.ptr());
+		if (XR_FAILED(result)) {
+			ERR_FAIL_V_MSG(PackedStringArray(), "OpenXR: Failed to obtain render model subaction paths [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+		}
+
+		for (uint32_t i = 0; i < capacity; i++) {
+			char buffer[1024];
+			uint32_t size = 0;
+			xrPathToString(instance, paths[i], 1024, &size, buffer);
+			if (size > 0) {
+				subaction_paths.push_back(String(buffer));
+			}
+		}
+	}
+
+	return subaction_paths;
+}
+
+XrPath OpenXRRenderModelExtension::render_model_get_top_level_path(RID p_render_model) const {
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, XRPose::TrackingConfidence::XR_TRACKING_CONFIDENCE_NONE);
+
+	return render_model->top_level_path;
+}
+
+String OpenXRRenderModelExtension::render_model_get_top_level_path_as_string(RID p_render_model) const {
+	String ret;
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	if (is_active() && has_render_model(p_render_model)) {
+		XrPath path = render_model_get_top_level_path(p_render_model);
+		if (path == XR_NULL_PATH) {
+			return "None";
+		} else {
+			return openxr_api->get_xr_path_name(path);
+		}
+	}
+
+	return ret;
+}
+
+XRPose::TrackingConfidence OpenXRRenderModelExtension::render_model_get_confidence(RID p_render_model) const {
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, XRPose::TrackingConfidence::XR_TRACKING_CONFIDENCE_NONE);
+
+	return render_model->confidence;
+}
+
+Transform3D OpenXRRenderModelExtension::render_model_get_root_transform(RID p_render_model) const {
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL_V(xr_server, Transform3D());
+
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, Transform3D());
+
+	// Scale our root transform
+	real_t world_scale = xr_server->get_world_scale();
+	Transform3D root_transform = render_model->root_transform.scaled(Vector3(world_scale, world_scale, world_scale));
+
+	return xr_server->get_reference_frame() * root_transform;
+}
+
+uint32_t OpenXRRenderModelExtension::render_model_get_animatable_node_count(RID p_render_model) const {
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, 0);
+
+	return render_model->animatable_node_count;
+}
+
+String OpenXRRenderModelExtension::render_model_get_animatable_node_name(RID p_render_model, uint32_t p_index) const {
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, String());
+
+	if (render_model->render_model_data.is_null()) {
+		// We never loaded it (don't spam errors here).
+		return String();
+	}
+
+	return render_model->render_model_data->get_node_name(p_index);
+}
+
+bool OpenXRRenderModelExtension::render_model_is_animatable_node_visible(RID p_render_model, uint32_t p_index) const {
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, false);
+
+	ERR_FAIL_UNSIGNED_INDEX_V(p_index, render_model->animatable_node_count, false);
+
+	if (render_model->node_states.is_empty()) {
+		// Never allocated (don't spam errors here).
+		return false;
+	}
+
+	return render_model->node_states[p_index].isVisible;
+}
+
+Transform3D OpenXRRenderModelExtension::render_model_get_animatable_node_transform(RID p_render_model, uint32_t p_index) const {
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Transform3D());
+
+	RenderModel *render_model = render_model_owner.get_or_null(p_render_model);
+	ERR_FAIL_NULL_V(render_model, Transform3D());
+
+	ERR_FAIL_UNSIGNED_INDEX_V(p_index, render_model->animatable_node_count, Transform3D());
+
+	if (render_model->node_states.is_empty()) {
+		// Never allocated (don't spam errors here).
+		return Transform3D();
+	}
+
+	return openxr_api->transform_from_pose(render_model->node_states[p_index].nodePose);
+}
+
+Ref<OpenXRRenderModelData> OpenXRRenderModelExtension::_get_render_model_data(XrUuidEXT p_cache_id, uint32_t p_animatable_node_count) {
+	if (render_model_data_cache.has(p_cache_id)) {
+		return render_model_data_cache[p_cache_id];
+	}
+
+	// We don't have this cached, lets load it up
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, nullptr);
+
+	XrSession session = openxr_api->get_session();
+	ERR_FAIL_COND_V(session == XR_NULL_HANDLE, nullptr);
+
+	XrRenderModelAssetEXT asset;
+
+	XrRenderModelAssetCreateInfoEXT create_info = {
+		XR_TYPE_RENDER_MODEL_ASSET_CREATE_INFO_EXT, // type
+		nullptr, // next
+		p_cache_id // cacheId
+	};
+
+	XrResult result = xrCreateRenderModelAssetEXT(session, &create_info, &asset);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(nullptr, "OpenXR: Failed to create render model asset [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	Ref<OpenXRRenderModelData> render_model_data = _load_asset(asset, p_animatable_node_count);
+
+	// We're done with this :)
+	result = xrDestroyRenderModelAssetEXT(asset);
+	if (XR_FAILED(result)) {
+		ERR_PRINT("OpenXR: Failed to destroy render model asset [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	// And cache it
+	render_model_data_cache[p_cache_id] = render_model_data;
+
+	return render_model_data;
+}
+
+Ref<OpenXRRenderModelData> OpenXRRenderModelExtension::_load_asset(XrRenderModelAssetEXT p_asset, uint32_t p_animatable_node_count) {
+	XrRenderModelAssetDataGetInfoEXT get_info = {
+		XR_TYPE_RENDER_MODEL_ASSET_DATA_GET_INFO_EXT, // type
+		nullptr, // next
+	};
+
+	XrRenderModelAssetDataEXT asset_data = {
+		XR_TYPE_RENDER_MODEL_ASSET_DATA_EXT, // type
+		nullptr, // next
+		0, // bufferCapacityInput;
+		0, // bufferCountOutput;
+		nullptr // buffer;
+	};
+
+	// Obtain required size for the buffer.
+	XrResult result = xrGetRenderModelAssetDataEXT(p_asset, &get_info, &asset_data);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(nullptr, "OpenXR: Failed to get render model buffer size [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+	ERR_FAIL_COND_V(asset_data.bufferCountOutput == 0, nullptr);
+
+	// Allocate data
+	PackedByteArray buffer;
+	buffer.resize(asset_data.bufferCountOutput);
+	asset_data.buffer = buffer.ptrw();
+	asset_data.bufferCapacityInput = asset_data.bufferCountOutput;
+
+	// Now get our actual data.
+	result = xrGetRenderModelAssetDataEXT(p_asset, &get_info, &asset_data);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(nullptr, "OpenXR: Failed to get render model buffer [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+	}
+
+	// Get the names of any animatable nodes
+	PackedStringArray node_names;
+	if (p_animatable_node_count > 0) {
+		Vector<XrRenderModelAssetNodePropertiesEXT> node_properties;
+		node_properties.resize(p_animatable_node_count);
+
+		XrRenderModelAssetPropertiesGetInfoEXT properties_info = {
+			XR_TYPE_RENDER_MODEL_ASSET_PROPERTIES_GET_INFO_EXT, // type
+			nullptr, // next
+		};
+
+		XrRenderModelAssetPropertiesEXT asset_properties = {
+			XR_TYPE_RENDER_MODEL_ASSET_PROPERTIES_EXT, // type
+			nullptr, // next
+			uint32_t(node_properties.size()), // nodePropertyCount
+			node_properties.ptrw(), // nodeProperties
+		};
+
+		result = xrGetRenderModelAssetPropertiesEXT(p_asset, &properties_info, &asset_properties);
+		if (XR_FAILED(result)) {
+			ERR_FAIL_V_MSG(nullptr, "OpenXR: Failed to get render model property info [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]");
+		}
+
+		node_names.resize(p_animatable_node_count);
+		String *node_names_ptrw = node_names.ptrw();
+		for (uint32_t i = 0; i < p_animatable_node_count; i++) {
+			node_names_ptrw[i] = String(node_properties[i].uniqueName);
+		}
+	}
+
+	Ref<OpenXRRenderModelData> render_model_data;
+	render_model_data.instantiate();
+
+	render_model_data->parse_gltf_document(buffer);
+	render_model_data->set_node_names(node_names);
+
+	return render_model_data;
+}
+
+void OpenXRRenderModelExtension::_clear_render_model_data() {
+	// Clear our toplevel paths filter.
+	toplevel_paths.clear();
+
+	// Clear our render model cache.
+	render_model_data_cache.clear();
+
+	// Loop through all of our render models and destroy them.
+	LocalVector<RID> owned = render_model_owner.get_owned_list();
+	for (const RID &rid : owned) {
+		render_model_destroy(rid);
+	}
+}
+
+bool OpenXRRenderModelData::parse_gltf_document(const PackedByteArray &p_bytes) {
+	// State holds our data, document parses GLTF
+	Ref<GLTFState> new_state;
+	new_state.instantiate();
+	Ref<GLTFDocument> new_gltf_document;
+	new_gltf_document.instantiate();
+
+	Error err = new_gltf_document->append_from_buffer(p_bytes, "", new_state);
+	if (err != OK) {
+		ERR_FAIL_V_MSG(false, "OpenXR: Failed to parse GLTF data.");
+	}
+
+	gltf_document = new_gltf_document;
+	gltf_state = new_state;
+	return true;
+}
+
+Node3D *OpenXRRenderModelData::new_scene_instance() {
+	ERR_FAIL_COND_V(gltf_document.is_null(), nullptr);
+	ERR_FAIL_COND_V(gltf_state.is_null(), nullptr);
+
+	return Object::cast_to<Node3D>(gltf_document->generate_scene(gltf_state));
+}
+
+void OpenXRRenderModelData::set_node_names(const PackedStringArray &p_node_names) {
+	node_names = p_node_names;
+}
+
+PackedStringArray OpenXRRenderModelData::get_node_names() const {
+	return node_names;
+}
+
+const String OpenXRRenderModelData::get_node_name(uint32_t p_node_index) const {
+	ERR_FAIL_UNSIGNED_INDEX_V(p_node_index, node_names.size(), String());
+
+	return node_names[p_node_index];
+}
+
+OpenXRRenderModelData::OpenXRRenderModelData() {
+}
+
+OpenXRRenderModelData::~OpenXRRenderModelData() {
+}

+ 168 - 0
modules/openxr/extensions/openxr_render_model_extension.h

@@ -0,0 +1,168 @@
+/**************************************************************************/
+/*  openxr_render_model_extension.h                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../openxr_uuid.h"
+#include "../util.h"
+#include "core/templates/rid_owner.h"
+#include "modules/gltf/gltf_document.h"
+#include "openxr_extension_wrapper.h"
+#include "scene/3d/node_3d.h"
+#include "servers/xr/xr_pose.h"
+
+#include <openxr/openxr.h>
+
+class OpenXRRenderModelData : public RefCounted {
+	GDCLASS(OpenXRRenderModelData, RefCounted);
+
+private:
+	Ref<GLTFDocument> gltf_document;
+	Ref<GLTFState> gltf_state;
+	PackedStringArray node_names;
+
+public:
+	Ref<GLTFState> get_gltf_state() { return gltf_state; }
+
+	bool parse_gltf_document(const PackedByteArray &p_bytes);
+	Node3D *new_scene_instance();
+
+	void set_node_names(const PackedStringArray &p_node_names);
+	PackedStringArray get_node_names() const;
+	const String get_node_name(uint32_t p_node_index) const;
+
+	OpenXRRenderModelData();
+	~OpenXRRenderModelData();
+};
+
+class OpenXRRenderModelExtension : public OpenXRExtensionWrapper {
+	GDCLASS(OpenXRRenderModelExtension, OpenXRExtensionWrapper);
+
+protected:
+	static void _bind_methods();
+
+public:
+	static OpenXRRenderModelExtension *get_singleton();
+
+	OpenXRRenderModelExtension();
+	virtual ~OpenXRRenderModelExtension() override;
+
+	virtual HashMap<String, bool *> get_requested_extensions() override;
+
+	virtual void on_instance_created(const XrInstance p_instance) override;
+	virtual void on_session_created(const XrSession p_session) override;
+	virtual void on_instance_destroyed() override;
+	virtual void on_session_destroyed() override;
+
+	virtual bool on_event_polled(const XrEventDataBuffer &event) override;
+	virtual void on_sync_actions() override;
+
+	bool is_active() const;
+
+	// Render model.
+	bool has_render_model(RID p_render_model) const;
+	RID render_model_create(XrRenderModelIdEXT p_render_model_id);
+	void render_model_destroy(RID p_render_model);
+
+	TypedArray<RID> render_model_get_all();
+	Node3D *render_model_new_scene_instance(RID p_render_model) const;
+	PackedStringArray render_model_get_subaction_paths(RID p_render_model);
+	XrPath render_model_get_top_level_path(RID p_render_model) const;
+	String render_model_get_top_level_path_as_string(RID p_render_model) const;
+	XRPose::TrackingConfidence render_model_get_confidence(RID p_render_model) const;
+	Transform3D render_model_get_root_transform(RID p_render_model) const;
+	uint32_t render_model_get_animatable_node_count(RID p_render_model) const;
+	String render_model_get_animatable_node_name(RID p_render_model, uint32_t p_index) const;
+	bool render_model_is_animatable_node_visible(RID p_render_model, uint32_t p_index) const;
+	Transform3D render_model_get_animatable_node_transform(RID p_render_model, uint32_t p_index) const;
+
+private:
+	static OpenXRRenderModelExtension *singleton;
+
+	// Related extensions.
+	bool uuid_ext = false;
+	bool render_model_ext = false;
+	bool interaction_render_model_ext = false;
+
+	// XrSync status
+	bool xr_sync_has_run = false;
+
+	// Interaction data.
+	bool _interaction_data_dirty = true;
+	HashMap<XrRenderModelIdEXT, RID> interaction_render_models;
+
+	void _clear_interaction_data();
+	bool _update_interaction_data();
+
+	// Render model.
+	Vector<XrPath> toplevel_paths;
+
+	struct RenderModel {
+		XrRenderModelIdEXT xr_render_model_id = XR_NULL_RENDER_MODEL_ID_EXT;
+		XrRenderModelEXT xr_render_model = XR_NULL_HANDLE;
+		uint32_t animatable_node_count = 0;
+		Ref<OpenXRRenderModelData> render_model_data;
+		XrSpace xr_space = XR_NULL_HANDLE;
+		XRPose::TrackingConfidence confidence = XRPose::TrackingConfidence::XR_TRACKING_CONFIDENCE_NONE;
+		Transform3D root_transform;
+		LocalVector<XrRenderModelNodeStateEXT> node_states;
+		XrPath top_level_path = XR_NULL_PATH;
+	};
+
+	mutable RID_Owner<RenderModel, true> render_model_owner;
+
+	// GLTF asset cache
+	HashMap<XrUuidEXT, Ref<OpenXRRenderModelData>, HashMapHasherXrUuidEXT> render_model_data_cache;
+
+	Ref<OpenXRRenderModelData> _get_render_model_data(XrUuidEXT p_cache_id, uint32_t p_animatable_node_count);
+	Ref<OpenXRRenderModelData> _load_asset(XrRenderModelAssetEXT p_asset, uint32_t p_animatable_node_count);
+	void _clear_render_model_data();
+
+	// GDScript/GDExtension passthroughs
+	RID _render_model_create(uint64_t p_render_model_id);
+
+	// OpenXR API call wrappers
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateRenderModelEXT, (XrSession), session, (const XrRenderModelCreateInfoEXT *), createInfo, (XrRenderModelEXT *), renderModel);
+	EXT_PROTO_XRRESULT_FUNC1(xrDestroyRenderModelEXT, (XrRenderModelEXT), renderModel);
+	EXT_PROTO_XRRESULT_FUNC3(xrGetRenderModelPropertiesEXT, (XrRenderModelEXT), renderModel, (const XrRenderModelPropertiesGetInfoEXT *), getInfo, (XrRenderModelPropertiesEXT *), properties);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateRenderModelSpaceEXT, (XrSession), session, (const XrRenderModelSpaceCreateInfoEXT *), createInfo, (XrSpace *), space);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateRenderModelAssetEXT, (XrSession), session, (const XrRenderModelAssetCreateInfoEXT *), createInfo, (XrRenderModelAssetEXT *), asset);
+	EXT_PROTO_XRRESULT_FUNC1(xrDestroyRenderModelAssetEXT, (XrRenderModelAssetEXT), asset);
+	EXT_PROTO_XRRESULT_FUNC3(xrGetRenderModelAssetDataEXT, (XrRenderModelAssetEXT), asset, (const XrRenderModelAssetDataGetInfoEXT *), getInfo, (XrRenderModelAssetDataEXT *), buffer);
+	EXT_PROTO_XRRESULT_FUNC3(xrGetRenderModelAssetPropertiesEXT, (XrRenderModelAssetEXT), asset, (const XrRenderModelAssetPropertiesGetInfoEXT *), getInfo, (XrRenderModelAssetPropertiesEXT *), properties);
+	EXT_PROTO_XRRESULT_FUNC3(xrGetRenderModelStateEXT, (XrRenderModelEXT), renderModel, (const XrRenderModelStateGetInfoEXT *), getInfo, (XrRenderModelStateEXT *), state);
+	EXT_PROTO_XRRESULT_FUNC5(xrEnumerateInteractionRenderModelIdsEXT, (XrSession), session, (const XrInteractionRenderModelIdsEnumerateInfoEXT *), getInfo, (uint32_t), renderModelIdCapacityInput, (uint32_t *), renderModelIdCountOutput, (XrRenderModelIdEXT *), renderModelIds);
+	EXT_PROTO_XRRESULT_FUNC5(xrEnumerateRenderModelSubactionPathsEXT, (XrRenderModelEXT), renderModel, (const XrInteractionRenderModelSubactionPathInfoEXT *), info, (uint32_t), pathCapacityInput, (uint32_t *), pathCountOutput, (XrPath *), paths);
+	EXT_PROTO_XRRESULT_FUNC3(xrGetRenderModelPoseTopLevelUserPathEXT, (XrRenderModelEXT), renderModel, (const XrInteractionRenderModelTopLevelUserPathGetInfoEXT *), info, (XrPath *), topLevelUserPath);
+
+	EXT_PROTO_XRRESULT_FUNC4(xrLocateSpace, (XrSpace), space, (XrSpace), baseSpace, (XrTime), time, (XrSpaceLocation *), location);
+	EXT_PROTO_XRRESULT_FUNC1(xrDestroySpace, (XrSpace), space);
+	EXT_PROTO_XRRESULT_FUNC5(xrPathToString, (XrInstance), instance, (XrPath), path, (uint32_t), bufferCapacityInput, (uint32_t *), bufferCountOutput, (char *), buffer);
+};

+ 19 - 1
modules/openxr/openxr_api.cpp

@@ -633,7 +633,7 @@ bool OpenXRAPI::create_instance() {
 	}
 
 	XrResult result = xrCreateInstance(&instance_create_info, &instance);
-	ERR_FAIL_COND_V_MSG(XR_FAILED(result), false, "Failed to create XR instance.");
+	ERR_FAIL_COND_V_MSG(XR_FAILED(result), false, "Failed to create XR instance [" + get_error_string(result) + "].");
 
 	// from this point on we can use get_error_string to get more info about our errors...
 
@@ -2902,6 +2902,20 @@ XrPath OpenXRAPI::get_xr_path(const String &p_path) {
 	return path;
 }
 
+String OpenXRAPI::get_xr_path_name(const XrPath &p_path) {
+	ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, String());
+
+	uint32_t size = 0;
+	char path_name[XR_MAX_PATH_LENGTH];
+
+	XrResult result = xrPathToString(instance, p_path, XR_MAX_PATH_LENGTH, &size, path_name);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(String(), "OpenXR: failed to get name for a path! [" + get_error_string(result) + "]");
+	}
+
+	return String(path_name);
+}
+
 RID OpenXRAPI::get_tracker_rid(XrPath p_path) {
 	for (const RID &tracker_rid : tracker_owner.get_owned_list()) {
 		Tracker *tracker = tracker_owner.get_or_null(tracker_rid);
@@ -3464,6 +3478,10 @@ bool OpenXRAPI::sync_action_sets(const Vector<RID> p_active_sets) {
 		interaction_profile_changed = false;
 	}
 
+	for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
+		wrapper->on_sync_actions();
+	}
+
 	return true;
 }
 

+ 1 - 0
modules/openxr/openxr_api.h

@@ -437,6 +437,7 @@ public:
 	void parse_velocities(const XrSpaceVelocity &p_velocity, Vector3 &r_linear_velocity, Vector3 &r_angular_velocity);
 	bool xr_result(XrResult result, const char *format, Array args = Array()) const;
 	XrPath get_xr_path(const String &p_path);
+	String get_xr_path_name(const XrPath &p_path);
 	bool is_top_level_path_supported(const String &p_toplevel_path);
 	bool is_interaction_profile_supported(const String &p_ip_path);
 	bool interaction_profile_supports_io_path(const String &p_ip_path, const String &p_io_path);

+ 52 - 0
modules/openxr/openxr_uuid.h

@@ -0,0 +1,52 @@
+/**************************************************************************/
+/*  openxr_uuid.h                                                         */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+// Godot helper functions for OpenXR XrUuidExt data type
+#include "core/templates/hashfuncs.h"
+
+#include <openxr/openxr.h>
+
+struct HashMapHasherXrUuidEXT {
+	static _FORCE_INLINE_ uint32_t hash(const XrUuidEXT &p_uuid) { return hash_murmur3_buffer(p_uuid.data, XR_UUID_SIZE_EXT); }
+};
+
+template <>
+struct HashMapComparatorDefault<XrUuidEXT> {
+	static bool compare(const XrUuidEXT &p_lhs, const XrUuidEXT &p_rhs) {
+		for (int i = 0; i < XR_UUID_SIZE_EXT; i++) {
+			if (p_lhs.data[i] != p_rhs.data[i]) {
+				return false;
+			}
+		}
+		return true;
+	}
+};

+ 11 - 0
modules/openxr/register_types.cpp

@@ -48,6 +48,8 @@
 #include "scene/openxr_composition_layer_cylinder.h"
 #include "scene/openxr_composition_layer_equirect.h"
 #include "scene/openxr_composition_layer_quad.h"
+#include "scene/openxr_render_model.h"
+#include "scene/openxr_render_model_manager.h"
 #include "scene/openxr_visibility_mask.h"
 
 #include "extensions/openxr_composition_layer_depth_extension.h"
@@ -69,6 +71,7 @@
 #include "extensions/openxr_palm_pose_extension.h"
 #include "extensions/openxr_performance_settings_extension.h"
 #include "extensions/openxr_pico_controller_extension.h"
+#include "extensions/openxr_render_model_extension.h"
 #include "extensions/openxr_valve_analog_threshold_extension.h"
 #include "extensions/openxr_visibility_mask_extension.h"
 #include "extensions/openxr_wmr_controller_extension.h"
@@ -122,6 +125,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
 		GDREGISTER_ABSTRACT_CLASS(OpenXRFutureResult); // Declared abstract, should never be instantiated by a user (Q or should this be internal?)
 		GDREGISTER_CLASS(OpenXRFutureExtension);
 		GDREGISTER_CLASS(OpenXRAPIExtension);
+		GDREGISTER_CLASS(OpenXRRenderModelExtension);
 
 		// Note, we're not registering all wrapper classes here, there is no point in exposing them
 		// if there isn't specific logic to expose.
@@ -159,6 +163,11 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
 			OpenXRAPI::register_extension_wrapper(future_extension);
 			Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRFutureExtension", future_extension));
 
+			// Register render model extension as a singleton.
+			OpenXRRenderModelExtension *render_model_extension = memnew(OpenXRRenderModelExtension);
+			OpenXRAPI::register_extension_wrapper(render_model_extension);
+			Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRRenderModelExtension", render_model_extension));
+
 			// register gated extensions
 			if (int(GLOBAL_GET("xr/openxr/extensions/debug_utils")) > 0) {
 				OpenXRAPI::register_extension_wrapper(memnew(OpenXRDebugUtilsExtension));
@@ -234,6 +243,8 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
 #endif
 
 		GDREGISTER_CLASS(OpenXRVisibilityMask);
+		GDREGISTER_CLASS(OpenXRRenderModel);
+		GDREGISTER_CLASS(OpenXRRenderModelManager);
 
 		XRServer *xr_server = XRServer::get_singleton();
 		if (xr_server) {

+ 168 - 0
modules/openxr/scene/openxr_render_model.cpp

@@ -0,0 +1,168 @@
+/**************************************************************************/
+/*  openxr_render_model.cpp                                               */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "openxr_render_model.h"
+
+#include "../extensions/openxr_render_model_extension.h"
+#include "core/config/project_settings.h"
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/3d/xr/xr_nodes.h"
+#include "scene/resources/3d/primitive_meshes.h"
+
+void OpenXRRenderModel::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_top_level_path"), &OpenXRRenderModel::get_top_level_path);
+
+	ClassDB::bind_method(D_METHOD("get_render_model"), &OpenXRRenderModel::get_render_model);
+	ClassDB::bind_method(D_METHOD("set_render_model", "render_model"), &OpenXRRenderModel::set_render_model);
+	ADD_PROPERTY(PropertyInfo(Variant::RID, "render_model"), "set_render_model", "get_render_model");
+
+	ADD_SIGNAL(MethodInfo("render_model_top_level_path_changed"));
+}
+
+void OpenXRRenderModel::_load_render_model_scene() {
+	OpenXRRenderModelExtension *render_model_extension = OpenXRRenderModelExtension::get_singleton();
+	ERR_FAIL_NULL(render_model_extension);
+	ERR_FAIL_COND(render_model.is_null());
+
+	scene = render_model_extension->render_model_new_scene_instance(render_model);
+	if (scene) {
+		// Get and cache our animatable nodes.
+		animatable_nodes.clear();
+		uint32_t count = render_model_extension->render_model_get_animatable_node_count(render_model);
+		for (uint32_t i = 0; i < count; i++) {
+			String node_name = render_model_extension->render_model_get_animatable_node_name(render_model, i);
+			if (!node_name.is_empty()) {
+				Node3D *child = Object::cast_to<Node3D>(scene->find_child(node_name));
+				if (child) {
+					animatable_nodes[node_name] = child;
+				}
+			}
+		}
+
+		// Now add to scene.
+		add_child(scene);
+	}
+}
+
+void OpenXRRenderModel::_on_render_model_top_level_path_changed(RID p_render_model) {
+	if (render_model == p_render_model) {
+		emit_signal("render_model_top_level_path_changed");
+	}
+}
+
+void OpenXRRenderModel::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			OpenXRRenderModelExtension *render_model_extension = OpenXRRenderModelExtension::get_singleton();
+			ERR_FAIL_NULL(render_model_extension);
+			if (render_model.is_valid()) {
+				_load_render_model_scene();
+			}
+
+			set_process_internal(true);
+			render_model_extension->connect("render_model_top_level_path_changed", callable_mp(this, &OpenXRRenderModel::_on_render_model_top_level_path_changed));
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			set_process_internal(false);
+
+			if (scene) {
+				animatable_nodes.clear();
+
+				remove_child(scene);
+				scene->queue_free();
+				scene = nullptr;
+			}
+
+			OpenXRRenderModelExtension *render_model_extension = OpenXRRenderModelExtension::get_singleton();
+			if (render_model_extension) {
+				render_model_extension->disconnect("render_model_top_level_path_changed", callable_mp(this, &OpenXRRenderModel::_on_render_model_top_level_path_changed));
+			}
+		} break;
+		case NOTIFICATION_INTERNAL_PROCESS: {
+			if (render_model.is_valid()) {
+				OpenXRRenderModelExtension *render_model_extension = OpenXRRenderModelExtension::get_singleton();
+				ERR_FAIL_NULL(render_model_extension);
+
+				if (render_model_extension->render_model_get_confidence(render_model) != XRPose::TrackingConfidence::XR_TRACKING_CONFIDENCE_NONE) {
+					set_transform(render_model_extension->render_model_get_root_transform(render_model));
+
+					if (scene) {
+						uint32_t count = render_model_extension->render_model_get_animatable_node_count(render_model);
+						for (uint32_t i = 0; i < count; i++) {
+							String node_name = render_model_extension->render_model_get_animatable_node_name(render_model, i);
+							if (!node_name.is_empty() && animatable_nodes.has(node_name)) {
+								Node3D *child = animatable_nodes[node_name];
+								child->set_visible(render_model_extension->render_model_is_animatable_node_visible(render_model, i));
+								child->set_transform(render_model_extension->render_model_get_animatable_node_transform(render_model, i));
+							}
+						}
+					}
+				}
+			}
+		} break;
+	}
+}
+
+String OpenXRRenderModel::get_top_level_path() const {
+	String ret;
+
+	OpenXRRenderModelExtension *render_model_extension = OpenXRRenderModelExtension::get_singleton();
+	if (render_model.is_valid() && render_model_extension) {
+		ret = render_model_extension->render_model_get_top_level_path_as_string(render_model);
+	}
+
+	return ret;
+}
+
+PackedStringArray OpenXRRenderModel::get_configuration_warnings() const {
+	PackedStringArray warnings;
+
+	Node *parent = get_parent();
+	if (!parent->is_class("XROrigin3D") && !parent->is_class("OpenXRRenderModelManager")) {
+		warnings.push_back("This node must be a child of either a XROrigin3D or OpenXRRenderModelManager node!");
+	}
+
+	if (!GLOBAL_GET("xr/openxr/extensions/render_model")) {
+		warnings.push_back("The render model extension is not enabled in project settings!");
+	}
+
+	return warnings;
+}
+
+RID OpenXRRenderModel::get_render_model() const {
+	return render_model;
+}
+
+void OpenXRRenderModel::set_render_model(RID p_render_model) {
+	render_model = p_render_model;
+	if (is_inside_tree() && render_model.is_valid()) {
+		_load_render_model_scene();
+	}
+}

+ 60 - 0
modules/openxr/scene/openxr_render_model.h

@@ -0,0 +1,60 @@
+/**************************************************************************/
+/*  openxr_render_model.h                                                 */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "scene/3d/node_3d.h"
+
+#include <openxr/openxr.h>
+
+class OpenXRRenderModel : public Node3D {
+	GDCLASS(OpenXRRenderModel, Node3D);
+
+private:
+	RID render_model;
+	Node3D *scene = nullptr;
+	HashMap<String, Node3D *> animatable_nodes;
+
+	void _load_render_model_scene();
+	void _on_render_model_top_level_path_changed(RID p_render_model);
+
+protected:
+	static void _bind_methods();
+
+	void _notification(int p_what);
+
+public:
+	virtual PackedStringArray get_configuration_warnings() const override;
+
+	RID get_render_model() const;
+	void set_render_model(RID p_render_model);
+
+	String get_top_level_path() const;
+};

+ 284 - 0
modules/openxr/scene/openxr_render_model_manager.cpp

@@ -0,0 +1,284 @@
+/**************************************************************************/
+/*  openxr_render_model_manager.cpp                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "openxr_render_model_manager.h"
+
+#include "../extensions/openxr_render_model_extension.h"
+#include "../openxr_api.h"
+#include "core/config/project_settings.h"
+#include "scene/3d/xr/xr_nodes.h"
+#include "servers/xr_server.h"
+
+void OpenXRRenderModelManager::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_tracker"), &OpenXRRenderModelManager::get_tracker);
+	ClassDB::bind_method(D_METHOD("set_tracker", "tracker"), &OpenXRRenderModelManager::set_tracker);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "tracker", PROPERTY_HINT_ENUM, "Any,None set,Left Hand,Right Hand"), "set_tracker", "get_tracker");
+
+	ClassDB::bind_method(D_METHOD("get_make_local_to_pose"), &OpenXRRenderModelManager::get_make_local_to_pose);
+	ClassDB::bind_method(D_METHOD("set_make_local_to_pose", "make_local_to_pose"), &OpenXRRenderModelManager::set_make_local_to_pose);
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "make_local_to_pose", PROPERTY_HINT_ENUM_SUGGESTION, "aim,grip"), "set_make_local_to_pose", "get_make_local_to_pose");
+
+	ADD_SIGNAL(MethodInfo("render_model_added", PropertyInfo(Variant::OBJECT, "render_model", PROPERTY_HINT_RESOURCE_TYPE, "OpenXRRenderModel")));
+	ADD_SIGNAL(MethodInfo("render_model_removed", PropertyInfo(Variant::OBJECT, "render_model", PROPERTY_HINT_RESOURCE_TYPE, "OpenXRRenderModel")));
+
+	BIND_ENUM_CONSTANT(RENDER_MODEL_TRACKER_ANY);
+	BIND_ENUM_CONSTANT(RENDER_MODEL_TRACKER_NONE_SET);
+	BIND_ENUM_CONSTANT(RENDER_MODEL_TRACKER_LEFT_HAND);
+	BIND_ENUM_CONSTANT(RENDER_MODEL_TRACKER_RIGHT_HAND);
+}
+
+bool OpenXRRenderModelManager::_has_filters() {
+	return tracker != 0;
+}
+
+void OpenXRRenderModelManager::_update_models() {
+	OpenXRRenderModelExtension *render_model_extension = OpenXRRenderModelExtension::get_singleton();
+	ERR_FAIL_NULL(render_model_extension);
+
+	// Make a copy of our current models.
+	HashMap<RID, Node3D *> org_render_models = render_models;
+
+	// Loop through our interaction data so we add new entries.
+	TypedArray<RID> render_model_rids = render_model_extension->render_model_get_all();
+	for (const RID rid : render_model_rids) {
+		bool filter = false;
+
+		if (tracker != 0) {
+			XrPath model_path = render_model_extension->render_model_get_top_level_path(rid);
+			if (model_path != xr_path) {
+				// ignore this.
+				filter = true;
+			}
+		}
+
+		if (!filter) {
+			if (render_models.has(rid)) {
+				org_render_models.erase(rid);
+			} else {
+				// Create our container node before adding our first render model.
+				if (container == nullptr) {
+					container = memnew(Node3D);
+					add_child(container);
+				}
+
+				OpenXRRenderModel *render_model = memnew(OpenXRRenderModel);
+				render_model->set_render_model(rid);
+				container->add_child(render_model);
+				render_models[rid] = render_model;
+
+				emit_signal(SNAME("render_model_added"), render_model);
+			}
+		}
+	}
+
+	// Remove models we no longer need.
+	for (const KeyValue<RID, Node3D *> &e : org_render_models) {
+		// We sent this just before removing.
+		emit_signal(SNAME("render_model_removed"), e.value);
+
+		if (container) {
+			container->remove_child(e.value);
+		}
+		e.value->queue_free();
+		render_models.erase(e.key);
+	}
+
+	is_dirty = false;
+}
+
+void OpenXRRenderModelManager::_on_render_model_added(RID p_render_model) {
+	if (_has_filters()) {
+		// We'll update this in internal process.
+		is_dirty = true;
+	} else {
+		// No filters? Do this right away.
+		_update_models();
+	}
+}
+
+void OpenXRRenderModelManager::_on_render_model_removed(RID p_render_model) {
+	if (_has_filters()) {
+		// We'll update this in internal process.
+		is_dirty = true;
+	} else {
+		// No filters? Do this right away.
+		_update_models();
+	}
+}
+
+void OpenXRRenderModelManager::_on_render_model_top_level_path_changed(RID p_path) {
+	if (_has_filters()) {
+		// We'll update this in internal process.
+		is_dirty = true;
+	}
+}
+
+void OpenXRRenderModelManager::_notification(int p_what) {
+	// Do not run in editor!
+	if (Engine::get_singleton()->is_editor_hint()) {
+		return;
+	}
+
+	OpenXRRenderModelExtension *render_model_extension = OpenXRRenderModelExtension::get_singleton();
+	ERR_FAIL_NULL(render_model_extension);
+	if (!render_model_extension->is_active()) {
+		return;
+	}
+
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			_update_models();
+
+			render_model_extension->connect(SNAME("render_model_added"), callable_mp(this, &OpenXRRenderModelManager::_on_render_model_added));
+			render_model_extension->connect(SNAME("render_model_removed"), callable_mp(this, &OpenXRRenderModelManager::_on_render_model_removed));
+			render_model_extension->connect(SNAME("render_model_top_level_path_changed"), callable_mp(this, &OpenXRRenderModelManager::_on_render_model_top_level_path_changed));
+
+			if (_has_filters()) {
+				set_process_internal(true);
+			}
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			render_model_extension->disconnect(SNAME("render_model_added"), callable_mp(this, &OpenXRRenderModelManager::_on_render_model_added));
+			render_model_extension->disconnect(SNAME("render_model_removed"), callable_mp(this, &OpenXRRenderModelManager::_on_render_model_removed));
+			render_model_extension->disconnect(SNAME("render_model_top_level_path_changed"), callable_mp(this, &OpenXRRenderModelManager::_on_render_model_top_level_path_changed));
+
+			set_process_internal(false);
+			is_dirty = false;
+		} break;
+		case NOTIFICATION_INTERNAL_PROCESS: {
+			if (is_dirty) {
+				_update_models();
+			}
+
+			if (positional_tracker.is_valid() && !make_local_to_pose.is_empty() && container) {
+				Ref<XRPose> pose = positional_tracker->get_pose(make_local_to_pose);
+				if (pose.is_valid()) {
+					container->set_transform(pose->get_adjusted_transform().affine_inverse());
+				} else {
+					container->set_transform(Transform3D());
+				}
+			}
+
+			if (!_has_filters()) {
+				// No need to keep calling this.
+				set_process_internal(false);
+			}
+		}
+	}
+}
+
+PackedStringArray OpenXRRenderModelManager::get_configuration_warnings() const {
+	PackedStringArray warnings;
+
+	XROrigin3D *parent = nullptr;
+	if (tracker == 0 || tracker == 1) {
+		if (!make_local_to_pose.is_empty()) {
+			warnings.push_back("Must specify a tracker to make node local to pose.");
+		}
+
+		parent = Object::cast_to<XROrigin3D>(get_parent());
+	} else {
+		Node *node = get_parent();
+		while (!parent && node) {
+			parent = Object::cast_to<XROrigin3D>(node);
+
+			node = node->get_parent();
+		}
+	}
+	if (!parent) {
+		warnings.push_back("This node must be a child of an XROrigin3D node!");
+	}
+
+	if (!GLOBAL_GET("xr/openxr/extensions/render_model")) {
+		warnings.push_back("The render model extension is not enabled in project settings!");
+	}
+
+	return warnings;
+}
+
+void OpenXRRenderModelManager::set_tracker(RenderModelTracker p_tracker) {
+	if (tracker != p_tracker) {
+		tracker = p_tracker;
+		is_dirty = true;
+
+		if (tracker == RENDER_MODEL_TRACKER_ANY || tracker == RENDER_MODEL_TRACKER_NONE_SET) {
+			xr_path = XR_NULL_PATH;
+		} else if (!Engine::get_singleton()->is_editor_hint()) {
+			XRServer *xr_server = XRServer::get_singleton();
+			OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+			if (openxr_api && xr_server) {
+				String toplevel_path;
+				String tracker_name;
+				if (tracker == RENDER_MODEL_TRACKER_LEFT_HAND) {
+					tracker_name = "left_hand";
+					toplevel_path = "/user/hand/left";
+				} else if (tracker == RENDER_MODEL_TRACKER_RIGHT_HAND) {
+					tracker_name = "right_hand";
+					toplevel_path = "/user/hand/right";
+				} else {
+					ERR_FAIL_MSG("Unsupported tracker value set.");
+				}
+
+				positional_tracker = xr_server->get_tracker(tracker_name);
+				if (positional_tracker.is_null()) {
+					WARN_PRINT("OpenXR: Can't find tracker " + tracker_name);
+				}
+
+				xr_path = openxr_api->get_xr_path(toplevel_path);
+				if (xr_path == XR_NULL_PATH) {
+					WARN_PRINT("OpenXR: Can't find path for " + toplevel_path);
+				}
+			}
+		}
+
+		// Even if we now no longer have filters, we must update at least once.
+		set_process_internal(true);
+	}
+}
+
+OpenXRRenderModelManager::RenderModelTracker OpenXRRenderModelManager::get_tracker() const {
+	return tracker;
+}
+
+void OpenXRRenderModelManager::set_make_local_to_pose(const String &p_action) {
+	if (make_local_to_pose != p_action) {
+		make_local_to_pose = p_action;
+
+		if (container) {
+			// Reset just in case. It'll be set to the correct transform
+			// in our process if required.
+			container->set_transform(Transform3D());
+		}
+	}
+}
+
+String OpenXRRenderModelManager::get_make_local_to_pose() const {
+	return make_local_to_pose;
+}

+ 84 - 0
modules/openxr/scene/openxr_render_model_manager.h

@@ -0,0 +1,84 @@
+/**************************************************************************/
+/*  openxr_render_model_manager.h                                         */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "openxr_render_model.h"
+
+#include "scene/3d/node_3d.h"
+#include "scene/resources/packed_scene.h"
+#include "servers/xr/xr_positional_tracker.h"
+
+#include <openxr/openxr.h>
+
+class OpenXRRenderModelManager : public Node3D {
+	GDCLASS(OpenXRRenderModelManager, Node3D);
+
+public:
+	enum RenderModelTracker {
+		RENDER_MODEL_TRACKER_ANY,
+		RENDER_MODEL_TRACKER_NONE_SET,
+		RENDER_MODEL_TRACKER_LEFT_HAND,
+		RENDER_MODEL_TRACKER_RIGHT_HAND,
+	};
+
+	virtual PackedStringArray get_configuration_warnings() const override;
+
+	void set_tracker(RenderModelTracker p_tracker);
+	RenderModelTracker get_tracker() const;
+
+	void set_make_local_to_pose(const String &p_action);
+	String get_make_local_to_pose() const;
+
+private:
+	HashMap<RID, Node3D *> render_models;
+	Node3D *container = nullptr;
+
+	bool is_dirty = false;
+	RenderModelTracker tracker = RENDER_MODEL_TRACKER_ANY;
+	String make_local_to_pose;
+
+	// cached values
+	Ref<XRPositionalTracker> positional_tracker;
+	XrPath xr_path = XR_NULL_PATH;
+
+	bool _has_filters();
+	void _on_render_model_added(RID p_render_model);
+	void _on_render_model_removed(RID p_render_model);
+	void _on_render_model_top_level_path_changed(RID p_path);
+	void _update_models();
+
+protected:
+	static void _bind_methods();
+
+	void _notification(int p_what);
+};
+
+VARIANT_ENUM_CAST(OpenXRRenderModelManager::RenderModelTracker);