Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Entità spaziali in OpenXR
Per qualsiasi applicazione di realtà aumentata è necessario accedere a informazioni del mondo reale ed essere in grado di tracciare posizioni reali. L'API per le entità spaziali di OpenXR è stata introdotta proprio per questo scopo.
Ha un design molto modulare. Il nucleo dell'API definisce come sono strutturate le entità del mondo reale, come sono individuate e come le informazioni su di esse sono memorizzate e consultate.
Sono aggiunte varie estensioni che implementano sistemi specifici come il tracciamento dei marcatori, il tracciamento dei piani e gli ancoraggi. Queste sono definite come capacità spaziali.
Ogni entità gestibile dal sistema è suddivisa in componenti più piccoli, il che rende più facile estendere il sistema e aggiungere nuove capacità.
I fornitori sono in grado di implementare ed esporre ulteriori funzionalità e tipi di componenti che si possono utilizzare con l'API principale. Per Godot, questi si possono implementare tramite estensioni. Però queste implementazioni sono fuori dallo scopo di questo manuale.
Infine, è importante notare che il sistema per entità spaziali utilizza funzioni asincrone. Ciò significa che è possibile avviare un processo ed essere informati del suo completamento in un secondo momento.
Configura
Per utilizzare le entità spaziali è necessario abilitare le relative impostazioni del progetto. Si possono trovare nella sezione OpenXR:
Impostazione |
Descrizione |
|---|---|
Abilitato |
Abilita il nucleo del sistema per le entità spaziali. È necessario abilitarla affinché ognuno dei sistemi di entità spaziali funzioni. |
Abilita ancoraggi spaziali |
Abilita la capacità di ancoraggio spaziale che consente di creare e tracciare ancoraggi spaziali. |
Abilita ancoraggi persistenti |
Abilita di rendere persistenti gli ancoraggi spaziali. Ciò significa che la loro posizione è memorizzata e la si può recuperare nelle sessioni successive. |
Abilita rilevamento degli ancoraggi integrato |
Abilita la nostra logica integrata di rilevamento degli ancoraggi, che recupererà automaticamente gli ancoraggi persistenti e ne regolerà il posizionamento quando il tracciamento viene aggiornato. |
Abilita tracciamento dei piani |
Abilita la capacità di tracciamento dei piani che consente di rilevare superfici come pavimenti, pareti, soffitti e tavoli. |
Abilita tracciamento dei piani integrato |
Attiva la nostra logica integrata di rilevamento dei piani, che reagirà automaticamente alla disponibilità di nuovi dati sui piani. |
Abilita tracciamento dei marcatori |
Abilita la nostra capacità di tracciamento dei marcatori, che consente di rivelare marcatori come codici QR, marcatori Aruco e tag April. |
Abilita tracciamento dei marcatori integrato |
Attiva la nostra logica integrata di rilevamento dei marcatori, che reagirà automaticamente all'individuamento di nuovi marcatori o al movimento dei marcatori esistenti attorno allo spazio del giocatore. |
Nota
Si noti che diversi dispositivi XR richiedono anche di impostare i flag di autorizzazione. Questi si dovranno abilitare nelle impostazioni della preimpostazione di esportazione.
Abilitare le diverse funzionalità attiva le API OpenXR corrispondenti, ma è necessaria una logica in più per interagire con questi dati. Per ogni sistema principale, disponiamo di una logica integrata che si può abilitare per farlo automaticamente.
Discuteremo del sistema di entità spaziali presupponendo che la logica integrata sia abilitata. Successivamente, esamineremo le API sottostanti e come è possibile implementarla autonomamente; però è importante notare che spesso ciò è eccessivo e che le API sottostanti sono esposte principalmente per consentire alle estensioni GDExtension di implementare capacità in più.
Creare il nostro primo gestore spaziale
Quando vengono rilevate o create entità spaziali, viene istanziato un oggetto OpenXRSpatialEntityTracker e registrato con l'XRServer.
Ogni tipo di entità spaziale implementerà la propria sottoclasse e potremo quindi reagire in modo diverso a ciascun tipo di entità.
In generale, istanzieremo diverse sotto-scene per ogni tipo di entità. Poiché gli oggetti tracker si possono usare con i nodi XRAnchor3D, queste sotto-scene dovrebbero avere un nodo di questo tipo come nodo radice.
Tutti i tracker di entità esporranno la propria posizione tramite la posa default.
Possiamo automatizzare la creazione di queste sotto-scene e la loro aggiunta all'albero di scene creando un oggetto gestore. Poiché tutte le posizioni sono locali al nodo XROrigin3D, dovremmo creare il nostro gestore come nodo figlio del nostro nodo di origine.
Di seguito è riportata la base dello script che implementa la logica del nostro gestore:
class_name SpatialEntitiesManager
extends Node3D
## Signals a new spatial entity node was added.
signal added_spatial_entity(node: XRNode3D)
## Signals a spatial entity node is about to be removed.
signal removed_spatial_entity(node: XRNode3D)
## Scene to instantiate for spatial anchor entities.
@export var spatial_anchor_scene: PackedScene
## Scene to instantiate for plane tracking spatial entities.
@export var plane_tracker_scene: PackedScene
## Scene to instantiate for marker tracking spatial entities.
@export var marker_tracker_scene: PackedScene
# Trackers we manage nodes for.
var _managed_nodes: Dictionary[XRTracker, XRAnchor3D]
# Enter tree is called whenever our node is added to our scene.
func _enter_tree():
# Connect to signals that inform us about tracker changes.
XRServer.tracker_added.connect(_on_tracker_added)
XRServer.tracker_updated.connect(_on_tracker_updated)
XRServer.tracker_removed.connect(_on_tracker_removed)
# Set up existing trackers.
var trackers : Dictionary = XRServer.get_trackers(XRServer.TRACKER_ANCHOR)
for tracker_name in trackers:
var tracker: XRTracker = trackers[tracker_name]
if tracker and tracker is OpenXRSpatialEntityTracker:
_add_tracker(tracker)
# Exit tree is called whenever our node is removed from our scene.
func _exit_tree():
# Clean up our signals.
XRServer.tracker_added.disconnect(_on_tracker_added)
XRServer.tracker_updated.disconnect(_on_tracker_updated)
XRServer.tracker_removed.disconnect(_on_tracker_removed)
# Clean up trackers.
for tracker in _managed_nodes:
removed_spatial_entity.emit(_managed_nodes[tracker])
remove_child(_managed_nodes[tracker])
_managed_nodes[tracker].queue_free()
_managed_nodes.clear()
# See if this tracker should be managed by us and add it.
func _add_tracker(tracker: OpenXRSpatialEntityTracker):
var new_node: XRAnchor3D
if _managed_nodes.has(tracker):
# Already being managed by us!
return
if tracker is OpenXRAnchorTracker:
# Note: Generally spatial anchors are controlled by the developer and
# are unlikely to be handled by our manager.
# But just for completeness we'll add it in.
if spatial_anchor_scene:
var new_scene = spatial_anchor_scene.instantiate()
if new_scene is XRAnchor3D:
new_node = new_scene
else:
push_error("Spatial anchor scene doesn't have an XRAnchor3D as a root node and can't be used!")
new_scene.free()
elif tracker is OpenXRPlaneTracker:
if plane_tracker_scene:
var new_scene = plane_tracker_scene.instantiate()
if new_scene is XRAnchor3D:
new_node = new_scene
else:
push_error("Plane tracking scene doesn't have an XRAnchor3D as a root node and can't be used!")
new_scene.free()
elif tracker is OpenXRMarkerTracker:
if marker_tracker_scene:
var new_scene = marker_tracker_scene.instantiate()
if new_scene is XRAnchor3D:
new_node = new_scene
else:
push_error("Marker tracking scene doesn't have an XRAnchor3D as a root node and can't be used!")
new_scene.free()
else:
# Type of spatial entity tracker we're not supporting?
push_warning("OpenXR Spatial Entities: Unsupported anchor tracker " + tracker.get_name() + " of type " + tracker.get_class())
if not new_node:
# No scene defined or able to be instantiated? We're done!
return
# Set up and add to our scene.
new_node.tracker = tracker.name
new_node.pose = "default"
_managed_nodes[tracker] = new_node
add_child(new_node)
added_spatial_entity.emit(new_node)
# A new tracker was added to our XRServer.
func _on_tracker_added(tracker_name: StringName, type: int):
if type == XRServer.TRACKER_ANCHOR:
var tracker: XRTracker = XRServer.get_tracker(tracker_name)
if tracker and tracker is OpenXRSpatialEntityTracker:
_add_tracker(tracker)
# A tracked managed by XRServer was changed.
func _on_tracker_updated(_tracker_name: StringName, _type: int):
# For now we ignore this, there aren't any changes here we need to react
# to and the instanced scene can react to this itself if needed.
pass
# A tracker was removed from our XRServer.
func _on_tracker_removed(tracker_name: StringName, type: int):
if type == XRServer.TRACKER_ANCHOR:
var tracker: XRTracker = XRServer.get_tracker(tracker_name)
if _managed_nodes.has(tracker):
# We emit this right before we remove it!
removed_spatial_entity.emit(_managed_nodes[tracker])
# Remove the node.
remove_child(_managed_nodes[tracker])
# Queue free the node.
_managed_nodes[tracker].queue_free()
# And remove from our managed nodes.
_managed_nodes.erase(tracker)
Ancoraggi spaziali
Gli ancoraggi spaziali ci consentono di mappare posizioni del mondo reale nel nostro mondo virtuale in modo tale che il runtime XR tenga traccia di queste posizioni e le aggiorni secondo necessità. Se supportati, gli ancoraggi si possono rendere persistenti, il che significa che saranno ricreati nella posizione corretta al prossimo avvio dell'applicazione.
You can think of use cases such as: - placing virtual windows around your space that are recreated when your application restarts - placing virtual objects on your table or on your walls and have them recreated
Gli ancoraggi spaziali sono tracciati tramite gli oggetti OpenXRAnchorTracker registrati con l'XRServer.
Quando serve, la posizione dell'ancoraggio spaziale sarà aggiornata automaticamente; la posa sul tracker correlato sarà aggiornata e quindi il nodo XRAnchor3D si riposizionerà.
Quando un ancoraggio spaziale è reso persistente, ad esso è assegnato un identificatore univoco universale (o UUID). Dovrai memorizzare questo identificatore insieme a tutte le informazioni necessarie per ricostruire la scena. Nell'esempio di codice riportato di seguito, ci limiteremo a chiamare le funzioni set_scene_path e get_scene_path, ma dovrai fornire le tue implementazioni per queste funzioni.
Per creare un ancoraggio persistente è necessario seguire un flusso specifico: - Creare l'ancoraggio spaziale - Attendere che lo stato di tracciamento cambi in ENTITY_TRACKING_STATE_TRACKING - Rendere l'ancoraggio persistente - Ottenere l'UUID e salvarlo
Quando viene rilevato un ancoraggio persistente esistente, viene aggiunto un nuovo tracker con l'UUID già impostato. È questa differenza nel flusso di lavoro che ci permette di reagire correttamente agli ancoraggi persistenti nuovi ed esistenti.
Nota
Se si annulla la persistenza di un ancoraggio, l'UUID viene distrutto ma l'ancoraggio non viene rimosso automaticamente. Sarà necessario reagire al completamento dell'annullamento e poi ripulirlo. Inoltre, si riceverà un errore se si tenta di distruggere un'ancoraggio che è ancora persistente.
Per completare il nostro sistema di ancoraggio, cominciamo creando una scena che imposteremo come scena da istanziare per gli ancoraggi sul nostro nodo di gestione spaziale.
Questa scena dovrebbe avere un nodo XRAnchor3D come radice, ma nient'altro. Aggiungeremo uno script che caricherà una sotto-scena contenente l'aspetto visivo effettivo del nostro ancoraggio, così da poter creare diversi ancoraggi nella nostra scena. Presumeremo che l'intenzione sia quella di rendere questi ancoraggi persistenti e salveremo il percorso di questa sotto-scena come metadato per il nostro UUID.
class_name OpenXRSpatialAnchor3D
extends XRAnchor3D
var anchor_tracker: OpenXRAnchorTracker
var child_scene: Node
var made_persistent: bool = false
## Return the scene path for our UUID.
func get_scene_path(p_uuid: String) -> String:
# Placeholder, implement this.
return ""
## Store our scene path for our UUID.
func set_scene_path(p_uuid: String, p_scene_path: String):
# Placeholder, implement this.
pass
## Remove info related to our UUID.
func remove_uuid(p_uuid: String):
# Placeholder, implement this.
pass
## Set our child scene for this anchor, call this when creating a new anchor.
func set_child_scene(p_child_scene_path: String):
var packed_scene: PackedScene = load(p_child_scene_path)
if not packed_scene:
return
child_scene = packed_scene.instantiate()
if not child_scene:
return
add_child(child_scene)
# Called when our tracking state changes.
func _on_spatial_tracking_state_changed(new_state) -> void:
if new_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_TRACKING and not made_persistent:
# Only attempt to do this once.
made_persistent = true
# This warning is optional if you don't want to rely on persistence.
if not OpenXRSpatialAnchorCapability.is_spatial_persistence_supported():
push_warning("Persistent spatial anchors are not supported on this device!")
return
# Make this persistent, this will notify that the UUID changed on the anchor,
# we can then store our scene path which we've already applied to our
# tracked scene.
OpenXRSpatialAnchorCapability.persist_anchor(anchor_tracker, RID(), Callable())
func _on_uuid_changed() -> void:
if anchor_tracker.uuid != "":
made_persistent = true
if child_scene:
# If we already have a subscene, save that with the UUID.
set_scene_path(anchor_tracker.uuid, child_scene.scene_file_path)
else:
# If we do not, look up the UUID in our stored cache.
var scene_path: String = get_scene_path(anchor_tracker.uuid)
if scene_path.is_empty():
# Give a warning that we don't have a scene file stored for this UUID.
push_warning("Unknown UUID given, can't determine child scene.")
# Load a default scene so we can at least see something.
set_child_scene("res://unknown_anchor.tscn")
return
set_child_scene(scene_path)
func _ready():
anchor_tracker = XRServer.get_tracker(tracker)
if anchor_tracker:
_on_uuid_changed()
anchor_tracker.spatial_tracking_state_changed.connect(_on_spatial_tracking_state_changed)
anchor_tracker.uuid_changed.connect(_on_uuid_changed)
Con la nostra scena dell'ancoraggio pronta, possiamo aggiungere un paio di funzioni al nostro script di gestione spaziale per creare o rimuovere gli ancoraggi:
...
## Create a new spatial anchor with the associated child scene.
## If persistent anchors are supported, this will be created as a persistent node
## and we will store the child scene path with the anchor's UUID for future recreation.
func create_spatial_anchor(p_transform: Transform3D, p_child_scene_path: String):
# Do we have anchor support?
if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported():
push_error("Spatial anchors are not supported on this device!")
return
# Adjust our transform to local space.
var t: Transform3D = global_transform.inverse() * p_transform
# Create anchor on our current manager.
var new_anchor = OpenXRSpatialAnchorCapability.create_new_anchor(t, RID())
if not new_anchor:
push_error("Couldn't create an anchor for %s." % [ p_child_scene_path ])
return
# Creating a new anchor should have resulted in an XRAnchor being added to the scene
# by our manager. We can thus continue assuming this has happened.
var anchor_scene = get_tracked_scene(new_anchor)
if not anchor_scene:
push_error("Couldn't locate anchor scene for %s, has the manager been configured with an applicable anchor scene?" % [ new_anchor.name ])
return
if not anchor_scene is OpenXRSpatialAnchor3D:
push_error("Anchor scene for %s is not an OpenXRSpatialAnchor3D scene, has the manager been configured with an applicable anchor scene?" % [ new_anchor.name ])
return
anchor_scene.set_child_scene(p_child_scene_path)
## Removes this spatial anchor from our scene.
## If the spatial anchor is persistent, the associated UUID will be cleared.
func remove_spatial_anchor(p_anchor: XRAnchor3D):
# Do we have anchor support?
if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported():
push_error("Spatial anchors are not supported on this device!")
return
var tracker: XRTracker = XRServer.get_tracker(p_anchor.tracker)
if tracker and tracker is OpenXRAnchorTracker:
var anchor_tracker: OpenXRAnchorTracker = tracker
if anchor_tracker.has_uuid() and OpenXRSpatialAnchorCapability.is_spatial_persistence_supported():
# If we have a UUID we should first make the anchor unpersistent
# and then remove it on its callback.
remove_uuid(anchor_tracker.uuid)
OpenXRSpatialAnchorCapability.unpersist_anchor(anchor_tracker, RID(), _on_unpersist_complete)
else:
# Otherwise we can just remove it.
# This will remove it from the XRServer, which in turn will trigger cleaning up our node.
OpenXRSpatialAnchorCapability.remove_anchor(tracker)
func _on_unpersist_complete(p_tracker: XRTracker):
# Our tracker is now no longer persistent, we can remove it.
OpenXRSpatialAnchorCapability.remove_anchor(p_tracker)
## Retrieve the scene we've added for a given tracker (if any).
func get_tracked_scene(p_tracker: XRTracker) -> XRNode3D:
for node in get_children():
if node is XRNode3D and node.tracker == p_tracker.name:
return node
return null
Nota
There seems to be a bit of magic going on in the code above. Whenever a spatial anchor is created or removed on our anchor capability, the related tracker object is created or destroyed. This results in the spatial manager adding or removing the child scene for this anchor. Hence we can rely on this here.
Tracciamento dei piani
Il tracciamento dei piani ci consente di rilevare superfici come muri, pavimenti, soffitti e tavoli nelle vicinanze del giocatore. Questi dati possono provenire da una cattura della stanza effettuata dall'utente in precedenza, oppure essere rilevati in tempo reale da sensori ottici. L'estensione per il tracciamento dei piani qui non fa distinzione.
Nota
Alcuni runtime XR richiedono estensioni del fornitore per abilitare e/o configurare questo processo, ma i dati saranno esposti attraverso tale estensione.
Il codice scritto sopra per il gestore spaziale rileverà già i nostri nuovi piani. Dobbiamo solo impostare una nuova scena e assegnarla al gestore spaziale.
Il nodo radice per questa scena deve essere un nodo XRAnchor3D. Aggiungeremo un nodo StaticBody3D come figlio e aggiungeremo un nodo CollisionShape3D e un nodo MeshInstance3D come figli del corpo statico.
Il corpo statico e la forma di collisione ci permetteranno di rendere il piano interattivo.
Il nodo istanza di mesh ci permette di applicare un materiale "perforante" al piano; se combinato con la proprietà passthrough, questo trasforma il nostro piano in un occlusore visivo. Alternativamente, possiamo assegnare un materiale che visualizzerà il piano per motivi di debug.
Configuriamo questo materiale come material_override sulla nostra MeshInstance3D. Per il nostro materiale "perforante", creiamo un ShaderMaterial e utilizziamo il seguente codice come codice dello shader:
shader_type spatial;
render_mode unshaded, shadow_to_opacity;
void fragment() {
ALBEDO = vec3(0.0, 0.0, 0.0);
}
Dobbiamo anche aggiungere uno script alla nostra scena per garantire che la collisione e la mesh siano applicate.
extends XRAnchor3D
var plane_tracker: OpenXRPlaneTracker
func _update_mesh_and_collision():
if plane_tracker:
# Place our static body using our offset so both collision
# and mesh are positioned correctly.
$StaticBody3D.transform = plane_tracker.get_mesh_offset()
# Set our mesh so we can occlude the surface.
$StaticBody3D/MeshInstance3D.mesh = plane_tracker.get_mesh()
# And set our shape so we can have things collide things with our surface.
$StaticBody3D/CollisionShape3D.shape = plane_tracker.get_shape()
func _ready():
plane_tracker = XRServer.get_tracker(tracker)
if plane_tracker:
_update_mesh_and_collision()
plane_tracker.mesh_changed.connect(_update_mesh_and_collision)
Se supportato dal runtime XR, sono disponibili metadati aggiuntivi che è possibile interrogare sull'oggetto plane tracker. In particolare, è importante notare la proprietà plane_label che, se disponibile, identifica il tipo di superficie. Consultare la documentazione della classe OpenXRPlaneTracker per ulteriori informazioni.
Tracciamento dei marcatori
Il tracciamento dei marcatori rileva marcatori specifici nel mondo reale. Solitamente sono immagini stampate, come i codici QR.
L'API espone il supporto per 4 diversi tipi di codici: codici QR, codici Micro QR, codici Aruco e tag April; tuttavia, i runtime XR non sono obbligati a supportarli tutti.
Quando vengono rilevati i marcatori, gli oggetti OpenXRMarkerTracker vengono istanziati e registrati con l'XRServer.
Il nostro codice esistente del gestore spaziale li rileva già; tutto ciò che dobbiamo fare è creare una scena con un nodo XRAnchor3D alla radice, salvarla e assegnarla al gestore spaziale come scena da istanziare per i marcatori.
Il tracker dei marcatori dovrebbe essere completamente configurato al momento dell'assegnazione, quindi tutto ciò che serve è una funzione _ready che reagisca ai dati del marcatore. Di seguito uno schema per il codice necessario:
extends XRAnchor3D
var marker_tracker: OpenXRMarkerTracker
func _ready():
marker_tracker = XRServer.get_tracker(tracker)
if marker_tracker:
match marker_tracker.marker_type:
OpenXRSpatialComponentMarkerList.MARKER_TYPE_QRCODE:
var data = marker_tracker.get_marker_data()
if data is String:
# Data is a QR code as a string, usually a URL.
pass
elif data is PackedByteArray:
# Data is binary, can be anything.
pass
OpenXRSpatialComponentMarkerList.MARKER_TYPE_MICRO_QRCODE:
var data = marker_tracker.get_marker_data()
if data is String:
# Data is a QR code as a string, usually a URL.
pass
elif data is PackedByteArray:
# Data is binary, can be anything.
pass
OpenXRSpatialComponentMarkerList.MARKER_TYPE_ARUCO:
# Use marker_tracker.marker_id to identify the marker.
pass
OpenXRSpatialComponentMarkerList.MARKER_TYPE_APRIL_TAG:
# Use marker_tracker.marker_id to identify the marker.
pass
Come possiamo vedere, i codici QR forniscono un blocco di dati che può essere una stringa o un array di byte. Aruco e tag April forniscono un ID che viene letto dal codice.
Sta al te determinare il modo migliore per collegare i dati del marcatore alla scena da caricare. Un esempio potrebbe essere quello di codificare il nome della risorsa che si desidera visualizzare in un codice QR.
Accesso al backend
Per la maggior parte dei casi, il sistema di base, assieme a eventuali estensioni del fornitore, dovrebbe essere quello che la maggior parte degli utenti utilizzerebbe così com'è.
Per coloro che stanno implementando estensioni del fornitore, o per coloro per i quali la logica integrata non basta, l'accesso al backend è fornito tramite un insieme di oggetti singleton.
Questi oggetti possono servire anche per interrogare le capacità supportate dal visore in uso. Abbiamo già aggiunto codice che le verifica nel nostro gestore spaziale e nel codice di ancoraggio spaziale nelle sezioni precedenti.
Nota
Il sistema di entità spaziali incapsulerà molte entità OpenXR in risorse che sono restituite come RID.
Fondamenta delle entità spaziali
La funzionalità fondamentale delle entità spaziali è esposta tramite il singleton OpenXRSpatialEntityExtension.
La logica specifica è esposta tramite capacità che introducono tipi di componenti specializzati e forniscono accesso a tipi specifici di entità; tuttavia, tutte utilizzano gli stessi meccanismi per accedere ai dati delle entità gestite dal sistema di entità spaziali.
Cominceremo dando un'occhiata ai singoli componenti che costituiscono il sistema fondamentale.
Contesti spaziali
Un contesto spaziale è l'oggetto principale attraverso il quale interroghiamo il sistema di entità spaziali. I contesti spaziali ci consentono di configurare il modo in cui interagiamo con una o più capacità.
Si consiglia di creare un contesto spaziale per ogni capacità con cui si desidera interagire; in effetti, è proprio ciò che fa Godot per la sua logica integrata.
Cominciamo impostando gli oggetti di configurazione delle capacità a cui desideriamo accedere. Ogni capacità abiliterà i componenti supportati per tale capacità. Le impostazioni determinano quali componenti saranno abilitati. Esamineremo questi oggetti di configurazione più in dettaglio quando esamineremo ciascuna capacità supportata.
La creazione di un contesto spaziale è un'azione asincrona. Ciò significa che chiediamo al runtime XR di creare un contesto spaziale e, in un secondo momento, il runtime XR ci fornirà il risultato.
Il seguente script rappresenta l'inizio del nostro esempio e si può aggiungere come nodo alla propria scena. Mostra la creazione di un contesto spaziale per il tracciamento dei piani e configura l'individuazione delle entità.
extends Node
var spatial_context: RID
func _set_up_spatial_context():
# Already set up?
if spatial_context:
return
# Not supported or we're not yet ready?
if not OpenXRSpatialPlaneTrackingCapability.is_supported():
return
# We'll use plane tracking as an example here, our configuration object
# here does not have any additional configuration. It just needs to exist.
var plane_capability : OpenXRSpatialCapabilityConfigurationPlaneTracking = OpenXRSpatialCapabilityConfigurationPlaneTracking.new()
var future_result : OpenXRFutureResult = OpenXRSpatialEntityExtension.create_spatial_context([ plane_capability ])
# Wait for async completion.
await future_result.completed
# Obtain our result.
spatial_context = future_result.get_spatial_context()
if spatial_context:
# Connect to our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery)
# Perform our initial discovery.
_on_perform_discovery(spatial_context)
func _enter_tree():
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
# Just in case our session hasn't started yet,
# call our spatial context creation on start.
openxr_interface.session_begun.connect(_set_up_spatial_context)
# And in case it is already up and running, call it already,
# it will exit if we've called it too early.
_set_up_spatial_context()
func _exit_tree():
if spatial_context:
# Disconnect from our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery)
# Free our spatial context, this will clean it up.
OpenXRSpatialEntityExtension.free_spatial_context(spatial_context)
spatial_context = RID()
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
openxr_interface.session_begun.disconnect(_set_up_spatial_context)
func _on_perform_discovery(p_spatial_context):
# See next section.
pass
Snapshot di individuazione
Una volta creato il contesto spaziale, il runtime XR inizierà a gestire le entità spaziali in base alla configurazione delle capacità specificate.
Per trovare nuove entità o per ottenere informazioni sulle entità esistenti, possiamo creare uno snapshot di individuazione. Questo indicherà al runtime XR di raccogliere dati specifici relativi a tutte le entità spaziali attualmente gestite dal contesto spaziale.
Questa funzione è asincrona poiché potrebbe volerci un po' di tempo per raccogliere i dati e fornire i risultati. In genere, è consigliabile effettuare uno snapshot di individuazione quando vengono trovate nuove entità. OpenXR emette un evento quando ci sono nuove entità da elaborare; ciò fa emettere il segnale spatial_discovery_recommended dal nostro singleton OpenXRSpatialEntityExtension.
Nota che nell'esempio di codice mostrato sopra, ci stiamo già connettendo a questo segnale e chiamando il metodo _on_perform_discovery sul nostro nodo. Implementiamolo ora:
...
var discovery_result : OpenXRFutureResult
func _on_perform_discovery(p_spatial_context):
# We get this signal for all spatial contexts, so exit if this is not for us.
if p_spatial_context != spatial_context:
return
# If we currently have an ongoing discovery result, cancel it.
if discovery_result:
discovery_result.cancel_discovery()
# Perform our discovery.
discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, [ \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_BOUNDED_2D, \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_PLANE_ALIGNMENT \
])
# Wait for async completion.
await discovery_result.completed
var snapshot : RID = discovery_result.get_spatial_snapshot()
if snapshot:
# Process our snapshot result.
_process_snapshot(snapshot)
# And clean up our snapshot.
OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
func _process_snapshot(p_snapshot):
# See further down.
pass
Nota che quando si chiama discover_spatial_entities si specifica un elenco di componenti. La richiesta di individuazione troverà qualsiasi entità gestita dal contesto spaziale e che possiede almeno uno dei componenti specificati.
Snapshot di aggiornamento
Effettuare uno snapshot di aggiornamento ci consente di ottenere informazioni aggiornate sulle entità che abbiamo già individuato in precedenza con il nostro snapshot di individuazione. Questa funzione è sincrona, ed è pensata principalmente per ottenere dati di stato e di posizionamento e la si può eseguire a ogni frame.
In generale, è opportuno effettuare snapshot di aggiornamento solo quando è probabile che le entità cambino o abbiano un ciclo di vita definito. Un buon esempio sono gli ancoraggi persistenti e i marcatori. Consulta la documentazione relativa a una specifica capacità per determinare se questo è necessario.
Però non è necessario per il tracciamento dei piani per completare il nostro esempio, ecco un esempio di come apparirebbe uno snapshot di aggiornamento per il tracciamento dei piani, qualora ne avessimo bisogno:
...
func _process(_delta):
if not spatial_context:
return
if entities.is_empty():
return
var entity_rids: Array[RID]
for entity_id in entities:
entity_rids.push_back(entities[entity_id].entity)
var snapshot : RID = OpenXRSpatialEntityExtension.update_spatial_entities(spatial_context, entity_rids, [ \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_BOUNDED_2D, \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_PLANE_ALIGNMENT \
])
if snapshot:
# Process our snapshot.
_process_snapshot(snapshot)
# And clean up our snapshot.
OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
Nota che nell'esempio qui riportato utilizziamo la stessa funzione _process_snapshot per elaborare lo snapshot. Questo ha senso nella maggior parte delle situazioni. Tuttavia, se i componenti specificati durante la creazione dello snapshot sono diversi tra lo snapshot di individuazione e lo snapshot di aggiornamento, è necessario tenere conto dei componenti diversi.
Querying snapshots
Once we have a snapshot we can run queries over that snapshot to obtain the data held within. The snapshot is guaranteed to remain unchanged until you free it.
Per ogni componente aggiunto al nostro snapshot, abbiamo un oggetto dati corrispondente. Questo oggetto dati ha una doppia funzione: aggiungendolo alla richiesta, ci assicuriamo che sia interrogato quel tipo di componente, ed è l'oggetto in cui vengono caricati i dati interrogati.
Esiste un oggetto dati speciale che deve essere sempre aggiunto al nostro elenco di richieste come prima voce, ovvero OpenXRSpatialQueryResultData. Questo oggetto conterrà una voce per ogni entità restituita, con il suo ID univoco e lo stato attuale dell'entità.
Per completare la nostra logica di individuazione, aggiungiamo quanto segue:
...
var entities : Dictionary[int, OpenXRSpatialEntityTracker]
func _process_snapshot(p_snapshot):
# Always include our query result data.
var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new()
# Add our bounded 2D component data.
var bounded2d_list : OpenXRSpatialComponentBounded2DList = OpenXRSpatialComponentBounded2DList.new()
# And our plane alignment component data.
var alignment_list : OpenXRSpatialComponentPlaneAlignmentList = OpenXRSpatialComponentPlaneAlignmentList.new()
if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, [ query_result_data, bounded2d_list, alignment_list]):
for i in query_result_data.get_entity_id_size():
var entity_id = query_result_data.get_entity_id(i)
var entity_state = query_result_data.get_entity_state(i)
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_STOPPED:
# This state should only appear when doing an update snapshot
# and tells us this entity is no longer tracked.
# We thus remove it from our dictionary which should result
# in the entity being cleaned up.
if entities.has(entity_id):
var entity_tracker : OpenXRSpatialEntityTracker = entities[entity_id]
entity_tracker.spatial_tracking_state = entity_state
XRServer.remove_tracker(entity_tracker)
entities.erase(entity_id)
else:
var entity_tracker : OpenXRSpatialEntityTracker
var register_with_xr_server : bool = false
if entities.has(entity_id):
entity_tracker = entities[entity_id]
else:
entity_tracker = OpenXRSpatialEntityTracker.new()
entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id)
entities[entity_id] = entity_tracker
register_with_xr_server = true
# Copy the state.
entity_tracker.spatial_tracking_state = entity_state
# If we're tracking, we should query the rest of our components.
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_TRACKING:
var center_pose : Transform3D = bounded2d_list.get_center_pose(i)
entity_tracker.set_pose("default", center_pose, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH)
# For this example I'm using OpenXRSpatialEntityTracker which does not
# hold further data. You should extend this class to store the additional
# state retrieved. For plane tracking this would be OpenXRPlaneTracker
# and we can store the following data in the tracker:
var size : Vector2 = bounded2d_list.get_size(i)
var alignment = alignment_list.get_plane_alignment(i)
else:
entity_tracker.invalidate_pose("default")
# We don't register our tracker until after we've set our initial data.
if register_with_xr_server:
XRServer.add_tracker(entity_tracker)
Nota
Nell'esempio precedente ci affidiamo a ENTITY_TRACKING_STATE_STOPPED per eliminare le entità spaziali che non sono più tracciate. Questo è disponibile solo con gli snapshot di aggiornamento.
Per le capacità che si basano esclusivamente su snapshot di individuazione, potrebbe essere opportuno effettuare un'eliminazione basata sulle entità che non fanno più parte dello snapshot, anziché basarsi sul cambio dello stato.
Entità spaziali
Con le informazioni di cui sopra, ora sappiamo come interrogare le nostre entità spaziali e ottenere informazioni su di esse, ma c'è un pochetto di più che dobbiamo esaminare sulle entità stesse.
In teoria, otteniamo tutti i nostri dati dagli snapshot, tuttavia OpenXR include un'API aggiuntiva che ci permette di creare un oggetto entità spaziale dal nostro ID entità. Finché questo oggetto esiste, il runtime di XR sa che stiamo utilizzando questa entità e che non è ancora eliminata. Questo è un prerequisito per richiedere un aggiornamento su questa entità.
Nel nostro codice di esempio lo facciamo chiamando OpenXRSpatialEntityExtension.make_spatial_entity.
Alcune API per entità spaziali creano automaticamente l'oggetto per noi. In questo caso, dobbiamo chiamare OpenXRSpatialEntityExtension.add_spatial_entity per registrare l'oggetto creato con la nostra implementazione.
Entrambe le funzioni restituiscono un RID che possiamo utilizzare in altre funzioni che richiedono il nostro oggetto entità.
Quando abbiamo finito possiamo chiamare OpenXRSpatialEntityExtension.free_spatial_entity.
Si noti che non lo abbiamo fatto nel nostro codice di esempio. Questo è gestito automaticamente quando l'istanza di OpenXRSpatialEntityTracker viene distrutta.
Capacità di ancoraggio spaziale
Gli ancoraggi spaziali sono gestiti dal nostro oggetto singleton OpenXRSpatialAnchorCapability. Dopo aver creato la sessione OpenXR, è possibile chiamare OpenXRSpatialAnchorCapability.is_spatial_anchor_supported per verificare se la capacità di ancoraggio spaziale è supportata dal proprio hardware.
La capacità di ancoraggio spaziale si allontana leggermente da quanto mostrato in precedenza.
Il sistema di ancoraggio spaziale ci permette di identificare, tracciare, persistere e condividere una posizione fisica. Ciò che lo rende diverso è che creiamo e distruggiamo l'ancoraggio, gestendone quindi il ciclo di vita.
Utilizziamo quindi il sistema di individuazione solo per individuare gli ancoraggi creati e persistenti nelle sessioni precedenti, oppure gli ancoraggi condivisi con noi.
Nota
La condivisione degli ancoraggi non è attualmente supportata nella specifica delle entità spaziali.
Come mostrato nell'esempio precedente, iniziamo sempre creando un contesto spaziale, ma ora utilizzando l'oggetto di configurazione OpenXRSpatialCapabilityConfigurationAnchor. Mostreremo un esempio di questo codice dopo aver discusso gli ambiti di persistenza. Prima, però, esamineremo la gestione degli ancoraggi locali.
Non ci sono differenze nella creazione di ancoraggi spaziali rispetto a quanto discusso riguardo la logica integrata. L'unica cosa importante è passare il proprio contesto spaziale come parametro a OpenXRSpatialAnchorCapability.create_new_anchor.
Rendere un ancoraggio persistente richiede di attendere che l'ancoraggio inizi a tracciare; ciò significa che è necessario effettuare richieste di aggiornamento per ogni ancoraggio creato, in modo da poter elaborare i cambiamenti di stato.
Per rendere persistenti gli ancoraggi, è necessario configurare anche un ambito di persistenza. Nel nucleo di OpenXR sono supportati due tipi di ambiti di persistenza:
Enumerazione |
Descrizione |
|---|---|
PERSISTENCE_SCOPE_SYSTEM_MANAGED |
Fornisce all'applicazione l'accesso in sola lettura (ovvero, le applicazioni non possono modificare questo archivio) alle entità spaziali persistenti e gestite dal sistema. L'applicazione può utilizzare l'UUID nel componente di persistenza di questo archivio per correlare le entità tra contesti spaziali e riavvii del dispositivo. |
PERSISTENCE_SCOPE_LOCAL_ANCHORS |
Le operazioni di persistenza e l'accesso ai dati sono limitati agli ancoraggi spaziali, sullo stesso dispositivo, per lo stesso utente e la stessa applicazione (tramite le funzioni persist_anchor e unpersist_anchor) |
Cominceremo con un nuovo script che gestirà i nostri ancoraggi spaziali. Sarà simile allo script presentato prima, ma con qualche differenza.
La prima è la creazione del nostro ambito di persistenza.
extends Node
var persistence_context : RID
func _set_up_persistence_context():
# Already set up?
if persistence_context:
# Check our spatial context.
_set_up_spatial_context()
return
# Not supported or we're not yet ready? Just exit.
if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported():
return
# If we can't use a persistence scope, just create our spatial context without one.
if not OpenXRSpatialAnchorCapability.is_spatial_persistence_supported():
_set_up_spatial_context()
return
var scope : int = 0
if OpenXRSpatialAnchorCapability.is_persistence_scope_supported(OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_LOCAL_ANCHORS):
scope = OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_LOCAL_ANCHORS
elif OpenXRSpatialAnchorCapability.is_persistence_scope_supported(OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_SYSTEM_MANAGED):
scope = OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_SYSTEM_MANAGED
else:
# Don't have a known persistence scope, report and just set up without it.
push_error("No known persistence scope is supported.")
_set_up_spatial_context()
return
# Create our persistence scope.
var future_result : OpenXRFutureResult = OpenXRSpatialAnchorCapability.create_persistence_context(scope)
if not future:
# Couldn't create persistence scope? Just set up without it.
_set_up_spatial_context()
return
# Now wait for our process to complete.
await future_result.completed
# Get our result.
persistence_context = future_result.get_result()
if persistence_context:
# Now set up our spatial context.
_set_up_spatial_context()
func _enter_tree():
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
# Just in case our session hasn't started yet,
# call our context creation on start beginning with our persistence scope.
openxr_interface.session_begun.connect(_set_up_persistence_context)
# And in case it is already up and running, call it already,
# it will exit if we've called it too early.
_set_up_persistence_context()
func _exit_tree():
if spatial_context:
# Disconnect from our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery)
# Free our spatial context, this will clean it up.
OpenXRSpatialEntityExtension.free_spatial_context(spatial_context)
spatial_context = RID()
if persistence_context:
# Free our persistence context...
OpenXRSpatialAnchorCapability.free_persistence_context(persistence_context)
persistence_context = RID()
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
openxr_interface.session_begun.disconnect(_set_up_persistence_context)
Una volta creato l'ambito di persistenza, possiamo creare il nostro contesto spaziale.
...
var spatial_context: RID
func _set_up_spatial_context():
# Already set up?
if spatial_context:
return
# Not supported or we're not yet set up.
if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported():
return
# Create our anchor capability.
var anchor_capability : OpenXRSpatialCapabilityConfigurationAnchor = OpenXRSpatialCapabilityConfigurationAnchor.new()
# And set up our persistence configuration object (if needed).
var persistence_config : OpenXRSpatialContextPersistenceConfig
if persistence_context:
persistence_config = OpenXRSpatialContextPersistenceConfig.new()
persistence_config.add_persistence_context(persistence_context)
var future_result : OpenXRFutureResultg = OpenXRSpatialEntityExtension.create_spatial_context([ anchor_capability ], persistence_config)
# Wait for async completion.
await future_result.completed
# Obtain our result.
spatial_context = future_result.get_spatial_context()
if spatial_context:
# Connect to our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery)
# Perform our initial discovery.
_on_perform_discovery(spatial_context)
La creazione dello snapshot di individuazione per i nostri ancoraggi è quasi identica a quella precedente, tuttavia ha senso creare lo snapshot solo per gli ancoraggi persistenti. Conosciamo già gli ancoraggi che abbiamo creato durante la sessione, vogliamo solo accedere a quelli provenienti dal runtime XR.
Vogliamo inoltre effettuare richieste di aggiornamento regolari; in questo caso siamo interessati solo allo stato, quindi vogliamo elaborare il nostro snapshot in modo leggermente diverso.
Il sistema di ancoraggio ci dà accesso a due componenti:
Componente |
Classe dati |
Descrizione |
|---|---|---|
COMPONENT_TYPE_ANCHOR |
Ci fornisce la posa (posizione + orientamento) di ogni ancoraggio |
|
COMPONENT_TYPE_PERSISTENCE |
Ci fornisce lo stato di persistenza e l'UUID di ogni ancoraggio |
...
var discovery_result : OpenXRFutureResult
var entities : Dictionary[int, OpenXRAnchorTracker]
func _on_perform_discovery(p_spatial_context):
# We get this signal for all spatial contexts, so exit if this is not for us.
if p_spatial_context != spatial_context:
return
# Skip this if we don't have a persistence context.
if not persistence_context:
return
# If we currently have an ongoing discovery result, cancel it.
if discovery_result:
discovery_result.cancel_discovery()
# Perform our discovery.
discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, [ \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_ANCHOR, \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_PERSISTENCE \
])
# Wait for async completion.
await discovery_result.completed
var snapshot : RID = discovery_result.get_spatial_snapshot()
if snapshot:
# Process our snapshot result.
_process_snapshot(snapshot, true)
# And clean up our snapshot.
OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
func _process(_delta):
if not spatial_context:
return
if entities.is_empty():
return
var entity_rids: Array[RID]
for entity_id in entities:
entity_rids.push_back(entities[entity_id].entity)
# We just want our anchor component here.
var snapshot : RID = OpenXRSpatialEntityExtension.update_spatial_entities(spatial_context, entity_rids, [ \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_ANCHOR, \
])
if snapshot:
# Process our snapshot.
_process_snapshot(snapshot)
# And clean up our snapshot.
OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
func _process_snapshot(p_snapshot, p_get_uuids):
pass
Infine possiamo elaborare il nostro snapshot. Si noti che stiamo usando OpenXRAnchorTracker come classe tracker, poiché questa include già di suo tutto il supporto per gli ancoraggi.
...
func _process_snapshot(p_snapshot, p_get_uuids):
var result_data : Array
# Always include our query result data.
var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new()
result_data.push_back(query_result_data)
# Add our anchor component data.
var anchor_list : OpenXRSpatialComponentAnchorList = OpenXRSpatialComponentAnchorList.new()
result_data.push_back(anchor_list)
# And our persistent component data.
var persistent_list : OpenXRSpatialComponentPersistenceList
if p_get_uuids:
# Only add this when we need it.
persistent_list = OpenXRSpatialComponentPersistenceList.new()
result_data.push_back(persistent_list)
if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, result_data):
for i in query_result_data.get_entity_id_size():
var entity_id = query_result_data.get_entity_id(i)
var entity_state = query_result_data.get_entity_state(i)
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_STOPPED:
# This state should only appear when doing an update snapshot
# and tells us this entity is no longer tracked.
# We thus remove it from our dictionary which should result
# in the entity being cleaned up.
if entities.has(entity_id):
var entity_tracker : OpenXRAnchorTracker = entities[entity_id]
entity_tracker.spatial_tracking_state = entity_state
XRServer.remove_tracker(entity_tracker)
entities.erase(entity_id)
else:
var entity_tracker : OpenXRAnchorTracker
var register_with_xr_server : bool = false
if entities.has(entity_id):
entity_tracker = entities[entity_id]
else:
entity_tracker = OpenXRAnchorTracker.new()
entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id)
entities[entity_id] = entity_tracker
register_with_xr_server = true
# Copy the state.
entity_tracker.spatial_tracking_state = entity_state
# If we're tracking, we update our position.
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_TRACKING:
var anchor_transform = anchor_list.get_entity_pose(i)
entity_tracker.set_pose("default", anchor_transform, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH)
else:
entity_tracker.invalidate_pose("default")
# But persistence data is a big exception, it can be provided even if we're not tracking.
if p_get_uuids:
var persistent_state = persistent_list.get_persistent_state(i)
if persistent_state == 1:
entity_tracker.uuid = persistent_list.get_persistent_uuid(i)
# We don't register our tracker until after we've set our initial data.
if register_with_xr_server:
XRServer.add_tracker(entity_tracker)
Capacità del tracciamento dei piani
Il tracciamento dei piani è gestito dalla classe singleton OpenXRSpatialPlaneTrackingCapability.
Dopo aver creato la sessione OpenXR, è possibile chiamare OpenXRSpatialPlaneTrackingCapability.is_supported per verificare se la funzionalità di tracciamento dei piani è supportata dal proprio hardware.
Sebbene abbiamo fornito sopra la maggior parte del codice per il tracciamento dei piani, di seguito presenteremo l'implementazione completa, che ha alcune piccole alterazioni. Non è necessario aggiornare gli snapshot qui, facciamo solo lo snapshot di rilevamento e implementiamo la nostra funzione di elaborazione.
Il tracciamento dei piano offre accesso a due componenti il cui supporto è garantito e a tre componenti opzionali.
Componente |
Classe dati |
Descrizione |
|---|---|---|
COMPONENT_TYPE_BOUNDED_2D |
Ci fornisce la posa centrale e il rettangolo di delimitazione per ciascun piano. |
|
COMPONENT_TYPE_PLANE_ALIGNMENT |
Ci fornisce l'allineamento di ciascun piano |
|
COMPONENT_TYPE_MESH_2D |
Ci fornisce una mesh 2D che forma ciascun piano |
|
COMPONENT_TYPE_POLYGON_2D |
Ci fornisce un poligono 2D che forma ogni piano |
|
COMPONENT_TYPE_PLANE_SEMANTIC_LABEL |
Ci fornisce un'identificazione di tipo di ciascun piano |
Il nostro oggetto di configurazione per il tracciamento dei piani abilita già tutti i componenti supportati, ma dovremo interrogarlo, quindi memorizzeremo la nostra istanza in una variabile membro. Possiamo usare il nostro oggetto tracker OpenXRPlaneTracker per memorizzare i dati del componente.
extends Node
var plane_capability : OpenXRSpatialCapabilityConfigurationPlaneTracking
var spatial_context: RID
var discovery_result : OpenXRFutureResult
var entities : Dictionary[int, OpenXRPlaneTracker]
func _set_up_spatial_context():
# Already set up?
if spatial_context:
return
# Not supported or we're not yet ready?
if not OpenXRSpatialPlaneTrackingCapability.is_supported():
return
# We'll use plane tracking as an example here, our configuration object
# here does not have any additional configuration. It just needs to exist.
plane_capability = OpenXRSpatialCapabilityConfigurationPlaneTracking.new()
var future_result : OpenXRFutureResult = OpenXRSpatialEntityExtension.create_spatial_context([ plane_capability ])
# Wait for async completion.
await future_result.completed
# Obtain our result.
spatial_context = future_result.get_spatial_context()
if spatial_context:
# Connect to our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery)
# Perform our initial discovery.
_on_perform_discovery(spatial_context)
func _enter_tree():
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
# Just in case our session hasn't started yet,
# call our spatial context creation on start.
openxr_interface.session_begun.connect(_set_up_spatial_context)
# And in case it is already up and running, call it already,
# it will exit if we've called it too early.
_set_up_spatial_context()
func _exit_tree():
if spatial_context:
# Disconnect from our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery)
# Free our spatial context, this will clean it up.
OpenXRSpatialEntityExtension.free_spatial_context(spatial_context)
spatial_context = RID()
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
openxr_interface.session_begun.disconnect(_set_up_spatial_context)
func _on_perform_discovery(p_spatial_context):
# We get this signal for all spatial contexts, so exit if this is not for us.
if p_spatial_context != spatial_context:
return
# If we currently have an ongoing discovery result, cancel it.
if discovery_result:
discovery_result.cancel_discovery()
# Perform our discovery.
discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, \
plane_capability.get_enabled_components())
# Wait for async completion.
await discovery_result.completed
var snapshot : RID = discovery_result.get_spatial_snapshot()
if snapshot:
# Process our snapshot result.
_process_snapshot(snapshot)
# And clean up our snapshot.
OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
func _process_snapshot(p_snapshot):
var result_data : Array
# Make a copy of the entities we've currently found.
var org_entities : PackedInt64Array
for entity_id in entities:
org_entities.push_back(entity_id)
# Always include our query result data.
var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new()
result_data.push_back(query_result_data)
# Add our bounded 2D component data.
var bounded2d_list : OpenXRSpatialComponentBounded2DList = OpenXRSpatialComponentBounded2DList.new()
result_data.push_back(bounded2d_list)
# And our plane alignment component data.
var alignment_list : OpenXRSpatialComponentPlaneAlignmentList = OpenXRSpatialComponentPlaneAlignmentList.new()
result_data.push_back(alignment_list)
# We need either a Mesh2D or a Polygon2D, we don't need both.
var mesh2d_list : OpenXRSpatialComponentMesh2DList
var polygon2d_list : OpenXRSpatialComponentPolygon2DList
if plane_capability.get_supports_mesh_2d():
mesh2d_list = OpenXRSpatialComponentMesh2DList.new()
result_data.push_back(mesh2d_list)
elif plane_capability.get_supports_polygons():
polygon2d_list = OpenXRSpatialComponentPolygon2DList.new()
result_data.push_back(polygon2d_list)
# And add our semantic labels if supported.
var label_list : OpenXRSpatialComponentPlaneSemanticLabelList
if plane_capability.get_supports_labels():
label_list = OpenXRSpatialComponentPlaneSemanticLabelList.new()
result_data.push_back(label_list)
if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, result_data):
for i in query_result_data.get_entity_id_size():
var entity_id = query_result_data.get_entity_id(i)
var entity_state = query_result_data.get_entity_state(i)
# Remove the entity from our original list.
if org_entities.has(entity_id):
org_entities.erase(entity_id)
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_STOPPED:
# We're not doing update snapshots so we shouldn't get this,
# but just to future proof:
if entities.has(entity_id):
var entity_tracker : OpenXRPlaneTracker = entities[entity_id]
entity_tracker.spatial_tracking_state = entity_state
XRServer.remove_tracker(entity_tracker)
entities.erase(entity_id)
else:
var entity_tracker : OpenXRPlaneTracker
var register_with_xr_server : bool = false
if entities.has(entity_id):
entity_tracker = entities[entity_id]
else:
entity_tracker = OpenXRPlaneTracker.new()
entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id)
entities[entity_id] = entity_tracker
register_with_xr_server = true
# Copy the state.
entity_tracker.spatial_tracking_state = entity_state
# If we're tracking, we should query the rest of our components.
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_TRACKING:
var center_pose : Transform3D = bounded2d_list.get_center_pose(i)
entity_tracker.set_pose("default", center_pose, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH)
entity_tracker.bounds_size = bounded2d_list.get_size(i)
entity_tracker.plane_alignment = alignment_list.get_plane_alignment(i)
if mesh2d_list:
entity_tracker.set_mesh_data( \
mesh2d_list.get_transform(i), \
mesh2d_list.get_vertices(p_snapshot, i), \
mesh2d_list.get_indices(p_snapshot, i))
elif polygon2d_list:
# The logic in our tracker will convert the polygon to a mesh.
entity_tracker.set_mesh_data( \
polygon2d_list.get_transform(i), \
polygon2d_list.get_vertices(p_snapshot, i))
else:
entity_tracker.clear_mesh_data()
if label_list:
entity_tracker.plane_label = label_list.get_plane_semantic_label(i)
else:
entity_tracker.invalidate_pose("default")
# We don't register our tracker until after we've set our initial data.
if register_with_xr_server:
XRServer.add_tracker(entity_tracker)
# Any entities we've got left over, we can remove.
for entity_id in org_entities:
var entity_tracker : OpenXRPlaneTracker = entities[entity_id]
entity_tracker.spatial_tracking_state = OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_STOPPED
XRServer.remove_tracker(entity_tracker)
entities.erase(entity_id)
Capacita di tracciamento dei marcatori
Il tracciamento dei marcatori è gestito dalla classe singleton OpenXRSpatialMarkerTrackingCapability.
Il tracciamento dei marcatori funziona in modo simile al tracciamento dei piani, solo che in questo caso tracciamo entità specifiche nel mondo reale in base a un codice stampato su un oggetto come un pezzo di carta.
Esistono diverse opzioni di tracciamento dei marcatori. OpenXR ne supporta 4 pronte all'uso; la tabella seguente fornisce maggiori informazioni e il nome della funzione con cui verificare se il visore supporta una determinata opzione:
Opzione |
Verificare supporto |
Oggetto di configurazione |
|---|---|---|
Tag April |
|
|
Aruco |
|
|
Codice QR |
|
|
Codice Micro QR |
|
Ciascuna opzione ha il proprio oggetto di configurazione che puoi utilizzare durante la creazione di un'entità spaziale.
I codici QR consentono di codificare una stringa che è decodificata dal runtime XR e resa accessibile quando viene rilevato un marcatore. Con i tag April e i marcatori Aruco, sono codificati dati binari, ai quali è possibile accedere quando viene rilevato un marcatore; tuttavia, è necessario configurare il rilevamento con il formato di decodifica corretto.
Come esempio, creeremo un contesto spaziale che troverà codici QR e marcatori Aruco.
extends Node
var qrcode_config : OpenXRSpatialCapabilityConfigurationQrCode
var aruco_config : OpenXRSpatialCapabilityConfigurationAruco
var spatial_context: RID
func _set_up_spatial_context():
# Already set up?
if spatial_context:
return
var configurations : Array
# Add our QR code configuration.
if not OpenXRSpatialMarkerTrackingCapability.qrcode_is_supported():
qrcode_config = OpenXRSpatialCapabilityConfigurationQrCode.new()
configurations.push_back(qrcode_config)
# Add our Aruco marker configuration.
if not OpenXRSpatialMarkerTrackingCapability.aruco_is_supported():
aruco_config = OpenXRSpatialCapabilityConfigurationAruco.new()
aruco_config.aruco_dict = OpenXRSpatialCapabilityConfigurationAruco.ARUCO_DICT_7X7_1000
configurations.push_back(aruco_config)
# Nothing supported?
if configurations.is_empty():
return
var future_result : OpenXRFutureResult = OpenXRSpatialEntityExtension.create_spatial_context(configurations)
# Wait for async completion.
await future_result.completed
# Obtain our result.
spatial_context = future_result.get_spatial_context()
if spatial_context:
# Connect to our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery)
# Perform our initial discovery.
_on_perform_discovery(spatial_context)
func _enter_tree():
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
# Just in case our session hasn't started yet,
# call our spatial context creation on start.
openxr_interface.session_begun.connect(_set_up_spatial_context)
# And in case it is already up and running, call it already,
# it will exit if we've called it too early.
_set_up_spatial_context()
func _exit_tree():
if spatial_context:
# Disconnect from our discovery signal.
OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery)
# Free our spatial context, this will clean it up.
OpenXRSpatialEntityExtension.free_spatial_context(spatial_context)
spatial_context = RID()
var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR")
if openxr_interface and openxr_interface.is_initialized():
openxr_interface.session_begun.disconnect(_set_up_spatial_context)
Ogni marcatore, a prescindere dal tipo, sarà composto da due componenti:
Componente |
Classe dati |
Descrizione |
|---|---|---|
COMPONENT_TYPE_MARKER |
Ci fornisce il tipo, l'ID (Aruco e April Tag) e/o i dati (codice QR) per ogni marcatore. |
|
COMPONENT_TYPE_BOUNDED_2D |
Ci fornisce la posa centrale e il rettangolo di delimitazione per ciascun piano. |
Aggiungiamo la nostra implementazione di individuazione:
...
var discovery_result : OpenXRFutureResult
var entities : Dictionary[int, OpenXRMarkerTracker]
func _on_perform_discovery(p_spatial_context):
# We get this signal for all spatial contexts, so exit if this is not for us.
if p_spatial_context != spatial_context:
return
# If we currently have an ongoing discovery result, cancel it.
if discovery_result:
discovery_result.cancel_discovery()
# Perform our discovery.
discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, [\
OpenXRSpatialEntityExtension.COMPONENT_TYPE_MARKER, \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_BOUNDED_2D \
])
# Wait for async completion.
await discovery_result.completed
var snapshot : RID = discovery_result.get_spatial_snapshot()
if snapshot:
# Process our snapshot result.
_process_snapshot(snapshot, true)
# And clean up our snapshot.
OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
func _process_snapshot(p_snapshot, bool p_is_discovery):
var result_data : Array
# Make a copy of the entities we've currently found.
var org_entities : PackedInt64Array
if p_is_discovery:
# Only on discovery will we check if we have untracked entities to clean up.
for entity_id in entities:
org_entities.push_back(entity_id)
# Always include our query result data.
var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new()
result_data.push_back(query_result_data)
# And our marker component data.
var marker_list : OpenXRSpatialComponentMarkerList
if p_is_discovery:
# Only on discovery do we check our marker data
marker_list = OpenXRSpatialComponentMarkerList.new()
result_data.push_back(marker_list)
# Add our bounded 2D component data.
var bounded2d_list : OpenXRSpatialComponentBounded2DList = OpenXRSpatialComponentBounded2DList.new()
result_data.push_back(bounded2d_list)
if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, result_data):
for i in query_result_data.get_entity_id_size():
var entity_id = query_result_data.get_entity_id(i)
var entity_state = query_result_data.get_entity_state(i)
# Remove the entity from our original list.
if org_entities.has(entity_id):
org_entities.erase(entity_id)
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_STOPPED:
# We should only get this when doing an update,
# and we'll remove our marker in that case.
if entities.has(entity_id):
var entity_tracker : OpenXRMarkerTracker = entities[entity_id]
entity_tracker.spatial_tracking_state = entity_state
XRServer.remove_tracker(entity_tracker)
entities.erase(entity_id)
else:
var entity_tracker : OpenXRMarkerTracker
var register_with_xr_server : bool = false
if entities.has(entity_id):
entity_tracker = entities[entity_id]
else:
entity_tracker = OpenXRMarkerTracker.new()
entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id)
entities[entity_id] = entity_tracker
register_with_xr_server = true
# Copy the state.
entity_tracker.spatial_tracking_state = entity_state
# If we're tracking, we should query the rest of our components.
if entity_state == OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_TRACKING:
var center_pose : Transform3D = bounded2d_list.get_center_pose(i)
entity_tracker.set_pose("default", center_pose, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH)
entity_tracker.bounds_size = bounded2d_list.get_size(i)
if p_is_discovery:
entity_tracker.marker_type = marker_list.get_marker_type(i)
entity_tracker.marker_id = marker_list.get_marker_id(i)
entity_tracker.marker_data = marker_list.get_marker_data(p_snapshot, i)
else:
entity_tracker.invalidate_pose("default")
# We don't register our tracker until after we've set our initial data.
if register_with_xr_server:
XRServer.add_tracker(entity_tracker)
if p_is_discovery:
# Any entities we've got left over, we can remove.
for entity_id in org_entities:
var entity_tracker : OpenXRMarkerTracker = entities[entity_id]
entity_tracker.spatial_tracking_state = OpenXRSpatialEntityTracker.ENTITY_TRACKING_STATE_STOPPED
XRServer.remove_tracker(entity_tracker)
entities.erase(entity_id)
E aggiungiamo la nostra funzionalità di aggiornamento:
...
func _process(_delta):
if not spatial_context:
return
if entities.is_empty():
return
var entity_rids: Array[RID]
for entity_id in entities:
entity_rids.push_back(entities[entity_id].entity)
# We just want our anchor component here.
var snapshot : RID = OpenXRSpatialEntityExtension.update_spatial_entities(spatial_context, entity_rids, [ \
OpenXRSpatialEntityExtension.COMPONENT_TYPE_BOUNDED_2D, \
])
if snapshot:
# Process our snapshot.
_process_snapshot(snapshot, false)
# And clean up our snapshot.
OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)