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.

Entidades espaciais OpenXR

Para qualquer tipo de aplicação de realidade aumentada, você precisa acessar informações do mundo real e ser capaz de rastrear localizações do mundo real. A API de entidades espaciais (spatial entities) do OpenXR foi introduzida exatamente para este propósito.

Ela possui um design muito modular. O núcleo da API define como as entidades do mundo real são estruturadas, como são encontradas e como as informações sobre elas são armazenadas e acessadas.

Várias extensões são adicionadas por cima, as quais implementam sistemas específicos, como rastreamento de marcadores (marker tracking), rastreamento de planos (plane tracking) e âncoras (anchors). Estas são referidas como capacidades espaciais (spatial capabilities).

Cada entidade que pode ser manipulada pelo sistema é dividida em componentes menores, o que torna fácil estender o sistema e adicionar novas capacidades.

Os fabricantes têm a capacidade de implementar e expor capacidades adicionais e tipos de componentes que podem ser usados com a API principal. No caso do Godot, estas podem ser implementadas em extensões. No entanto, essas implementações estão fora do escopo deste manual.

Finalmente, é importante notar que o sistema de entidades espaciais faz uso de funções assíncronas. Isso significa que você pode iniciar um processo e ser informado sobre a sua conclusão mais tarde.

Configurar

Para usar entidades espaciais, você precisa ativar as configurações de projeto relacionadas. Você pode encontrá-las na seção OpenXR:

../../_images/openxr_spatial_entities_project_settings.webp
Configurações de entidade espacial

Configuração

Descrição

Habilitado

Ativa o núcleo do sistema de entidades espaciais. Isso deve estar ativado para que qualquer um dos sistemas de entidades espaciais funcione.

Habilitar âncoras espaciais

Ativa a capacidade de âncoras espaciais (spatial anchors) que permite criar e rastrear âncoras espaciais.

Habilitar âncoras persistentes

Ativa a capacidade de tornar as âncoras espaciais persistentes. Isso significa que a localização delas é armazenada e pode ser recuperada em sessões subsequentes.

Ativar a detecção de âncoras embutida

Ativa nossa lógica de detecção de âncoras embutida; isso recuperará automaticamente âncoras persistentes e ajustará o posicionamento das âncoras quando o rastreamento for atualizado.

Habilitar rastreamento de planos

Ativa a capacidade de rastreamento de planos que permite a detecção de superfícies como pisos, paredes, tetos e mesas.

Habilitar detecção de planos embutida

Ativa nossa lógica de detecção de planos embutida; isso reagirá automaticamente à disponibilização de novos dados de plano.

Ativar rastreamento de marcadores

Ativa nossa capacidade de rastreamento de marcadores que permite a detecção de marcadores como códigos QR, marcadores Aruco e AprilTags.

Ativar rastreamento de marcadores embutido

Ativa nossa lógica de detecção de marcadores embutida; isso reagirá automaticamente a novos marcadores encontrados ou a marcadores sendo movidos pelo espaço do jogador.

Nota

Note que vários dispositivos XR também exigem que as flags de permissão sejam definidas. Estas precisarão ser ativadas nas configurações de predefinição de exportação (export preset settings).

A ativação das diferentes capacidades ativa as APIs do OpenXR relacionadas, mas uma lógica adicional é necessária para interagir com esses dados. Para cada sistema principal, temos uma lógica embutida que pode ser ativada e que fará isso por você.

Discutiremos o sistema de entidades espaciais sob a suposição de que a lógica embutida seja ativada primeiro. Em seguida, daremos uma olhada nas APIs subjacentes e em como você mesmo pode implementar isso; no entanto, deve-se notar que isso frequentemente é um exagero (overkill) e que as APIs subjacentes são expostas principalmente para permitir que plugins GDExtension implementem capacidades adicionais.

Criando nosso gerenciador espacial

Quando entidades espaciais são detectadas ou criadas, um objeto OpenXRSpatialEntityTracker é instanciado e registrado no XRServer.

Cada tipo de entidade espacial implementará sua própria subclasse e podemos, assim, reagir de forma diferente a cada tipo de entidade.

Generally speaking we will instantiate different subscenes for each type of entity. As the tracker objects can be used with XRAnchor3D nodes, these subscenes should have such a node as their root node.

Todos os rastreadores de entidade exporão sua localização através da pose default.

Podemos automatizar a criação dessas subcenas e a adição delas à nossa árvore de cena criando um objeto gerenciador. Como todas as localizações são locais para o nó XROrigin3D, devemos criar nosso gerenciador como um nó filho do nosso nó de origem.

Abaixo está a base do script que implementa a lógica do nosso gerenciador:

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 instantiated 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)

Âncoras espaciais

As âncoras espaciais nos permitem mapear localizações do mundo real em nosso mundo virtual de tal forma que o runtime de XR acompanhará essas localizações e as ajustará conforme necessário. Se houver suporte, as âncoras podem se tornar persistentes, o que significa que as âncoras serão recriadas na localização correta quando seu aplicativo for iniciado novamente.

Você pode pensar em casos de uso como: - posicionar janelas virtuais ao redor do seu espaço que são recriadas quando seu aplicativo é reiniciado - posicionar objetos virtuais na sua mesa ou nas suas paredes e fazer com que sejam recriados

As âncoras espaciais são rastreadas usando objetos OpenXRAnchorTracker registrados no XRServer.

Quando necessário, a localização da âncora espacial será atualizada automaticamente; a pose no rastreador relacionado será atualizada e, assim, o nó XRAnchor3D se reposicionará.

Quando uma âncora espacial se torna persistente, um Identificador Único Universal (ou UUID) é atribuído à âncora. Você precisará armazenar isso junto com as informações necessárias para reconstruir a cena. Em nosso código de exemplo abaixo, simplesmente chamaremos set_scene_path e get_scene_path, mas você precisará fornecer suas próprias implementações para essas funções.

Para criar uma âncora persistente, você precisa seguir um fluxo específico: - Criar a âncora espacial - Aguardar até que o status de rastreamento mude para ENTITY_TRACKING_STATE_TRACKING - Tornar a âncora permanente - Obter o UUID e salvá-lo

Quando uma âncora persistente existente é encontrada, um novo rastreador é adicionado com o UUID já definido. É essa diferença no fluxo de trabalho que nos permite reagir corretamente a âncoras persistentes novas e existentes.

Nota

Se você remover a persistência (unpersist) de uma âncora, o UUID é destruído, mas a âncora não é removida automaticamente. Você precisará reagir à conclusão da remoção de persistência de uma âncora e então limpá-la. Além disso, você receberá um erro se tentar destruir uma âncora que ainda é persistente.

Para completar nosso sistema de âncoras, começamos criando uma cena que definiremos como a cena a ser instanciada para âncoras em nosso nó de gerenciador espacial.

Esta cena deve ter um nó XRAnchor3D como raiz, mas nada mais. Adicionaremos um script a ela que carregará uma subcena contendo o aspecto visual real da nossa âncora, para que possamos criar âncoras diferentes em nossa cena. Assumiremos que a intenção é tornar essas âncoras persistentes e salvar o caminho para esta subcena como metadados para o nosso 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)

Com nossa cena de âncora configurada, podemos adicionar algumas funções ao script do nosso gerenciador espacial para criar ou remover âncoras:

...

## 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

Parece haver um pouco de mágica acontecendo no código acima. Sempre que uma âncora espacial é criada ou removida na nossa capacidade de âncora, o objeto rastreador relacionado é criado ou destruído. Isso faz com que o gerenciador espacial adicione ou remova a cena filha para esta âncora. Portanto, podemos confiar nisso aqui.

Rastreamento de planos

O rastreamento de planos permite detectar superfícies como paredes, pisos, tetos e mesas nas proximidades do jogador. Esses dados podem vir de uma captura de ambiente realizada pelo usuário em qualquer momento no passado ou detectados ao vivo por sensores ópticos. A extensão de rastreamento de planos não faz distinção aqui.

Nota

Alguns runtimes de XR exigem extensões de fabricantes para ativar e/ou configurar esse processo, mas os dados serão expostos através desta extensão.

O código que escrevemos acima para o gerenciador espacial já detectará nossos novos planos. Precisamos configurar uma nova cena e atribuir essa cena ao gerenciador espacial.

O nó raiz para esta cena deve ser um nó XRAnchor3D. Adicionaremos um nó StaticBody3D como filho e adicionaremos um nó CollisionShape3D e um nó MeshInstance3D como filhos do corpo estático.

../../_images/openxr_plane_anchor.webp

O corpo estático e a forma de colisão nos permitirão tornar o plano interagível.

O nó de instância de malha (mesh instance) nos permite aplicar um material de "hole punch" ao plano; quando combinado com o passthrough, isso transforma nosso plano em um oclusor visual. Alternativamente, podemos atribuir um material que visualizará o plano para fins de depuração.

Configuramos este material como o material de sobreposição material_override em nosso MeshInstance3D. Para o nosso material de "recorte/furo" (hole punch), crie um ShaderMaterial e use o seguinte código como o código do shader:

shader_type spatial;
render_mode unshaded, shadow_to_opacity;

void fragment() {
    ALBEDO = vec3(0.0, 0.0, 0.0);
}

Também precisamos adicionar um script à nossa cena para garantir que nossa colisão e malha sejam aplicadas.

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 suportado pelo runtime de XR, há metadados adicionais que você pode consultar no objeto rastreador de plano. Destaca-se a propriedade plane_label que, se disponível, identifica o tipo de superfície. Por favor, consulte a documentação da classe OpenXRPlaneTracker para mais informações.

Rastreamento de marcadores

O rastreamento de marcadores detecta marcadores específicos no mundo real. Geralmente são imagens impressas, como códigos QR.

A API expõe suporte para 4 códigos diferentes: códigos QR, códigos Micro QR, códigos Aruco e AprilTags, no entanto, os runtimes de XR não são obrigados a suportar todos eles.

Quando marcadores são detectados, objetos OpenXRMarkerTracker são instanciados e registrados no XRServer.

Nosso código existente do gerenciador espacial já detecta estes marcadores; tudo o que precisamos fazer é criar uma cena com um nó XRAnchor3D na raiz, salvá-la e atribuí-la ao gerenciador espacial como a cena a ser instanciada para marcadores.

O rastreador de marcadores deve estar totalmente configurado quando atribuído, então tudo o que é necessário é uma função _ready que reaja aos dados do marcador. Abaixo está um modelo para o código necessário:

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

Como podemos ver, os códigos QR fornecem um bloco de dados que é uma string ou uma matriz de bytes. As tags Aruco e April fornecem um ID que é lido a partir do código.

Fica a critério do seu caso de uso a melhor forma de vincular os dados do marcador à cena que precisa ser carregada. Um exemplo seria codificar o nome do asset que você deseja exibir em um código QR.

Acesso ao backend

Para a maioria dos propósitos, o sistema principal, juntamente com quaisquer extensões de fabricantes, deve ser o que a maioria dos usuários utilizaria conforme fornecido.

Para aqueles que estão implementando extensões de fabricantes, ou aqueles para quem a lógica embutida não é suficiente, o acesso ao backend é fornecido através de um conjunto de objetos singleton.

Estes objetos também podem ser usados para consultar quais capacidades são suportadas pelo headset em uso. Já adicionamos código que verifica por elas em nosso gerenciador espacial e no código de âncora espacial nas seções acima.

Nota

O sistema de entidades espaciais encapsulará muitas entidades do OpenXR em recursos que são retornados como RIDs.

Núcleo de entidade espacial

A funcionalidade principal de entidade espacial é exposta através do singleton OpenXRSpatialEntityExtension.

Specific logic is exposed through capabilities that introduce specialized component types, and give access to specific types of entities, however they all use the same mechanisms for accessing the entity data managed by the spatial entity system.

Começaremos dando uma olhada nos componentes individuais que compõem o sistema principal.

Contextos espaciais

Um contexto espacial (spatial context) é o objeto principal através do qual consultamos o sistema de entidades espaciais. Os contextos espaciais nos permitem configurar como interagimos com uma ou mais capacidades.

Recomenda-se criar un contexto espacial para cada capacidade com a qual você deseja interagir; de fato, é isso que o Godot faz para sua lógica embutida.

Começamos definindo os objetos de configuração de capacidade para as capacidades que desejamos acessar. Cada capacidade ativará os componentes que suportamos para aquela capacidade. As configurações podem determinar quais componentes serão ativados. Olharemos para esses objetos de configuração em mais detalhes à medida que analisarmos cada capacidade suportada.

Criar um contexto espacial é uma ação assíncrona. Isso significa que solicitamos ao runtime de XR que crie um contexto espacial e, em um momento no futuro, o runtime de XR nos fornecerá o resultado.

O seguinte script é o início do nosso exemplo e pode ser adicionado como um nó à sua cena. Ele mostra a criação de um contexto espacial para rastreamento de planos e configura nossa descoberta de entidades.

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

Instantâneos de descoberta (Discovery snapshots)

Assim que nosso contexto espacial for criado, o runtime de XR começará a gerenciar entidades espaciais de acordo com a configuração das capacidades especificadas.

Para encontrar novas entidades, ou para obter informações sobre nossas entidades atuais, podemos criar um instantâneo de descoberta (discovery snapshot). Isso dirá ao runtime de XR para reunir dados específicos relacionados a todas as entidades espaciais gerenciadas atualmente pelo contexto espacial.

Esta função é assíncrona, pois pode levar algum tempo para reunir esses dados e oferecer seus resultados. De modo geral, você desejará realizar um instantâneo de descoberta quando novas entidades forem encontradas. O OpenXR emite um evento quando há novas entidades a serem processadas, o que resulta no sinal spatial_discovery_recommended sendo emitido pelo nosso singleton OpenXRSpatialEntityExtension.

Note que no código de exemplo mostrado acima, já estamos nos conectando a este sinal e chamando o método _on_perform_discovery em nosso nó. Vamos implementar isso:

...

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

Note que ao chamar discover_spatial_entities, especificamos uma lista de componentes. A consulta de descoberta encontrará qualquer entidade que seja gerenciada pelo contexto espacial e tenha pelo menos um dos componentes especificados.

Instantâneos de atualização (Update snapshots)

Realizar um instantâneo de atualização (update snapshot) nos permite obter informações atualizadas sobre entidades que já encontramos anteriormente com nosso instantâneo de descoberta. Esta função é síncrona e serve principalmente para obter dados de status e posicionamento, podendo ser executada a cada quadro.

De modo geral, você só realizaria instantâneos de atualização quando for provável que as entidades mudem ou tenham um processo de tempo de vida. Um bom exemplo disso são as âncoras persistentes e os marcadores. Consulte a documentação sobre uma capacidade para determinar se isso é necessário.

Não é necessário para o rastreamento de planos, no entanto, para completar o nosso exemplo, aqui está um exemplo de como seria um instantâneo de atualização para rastreamento de planos, caso precisássemos de um:

...

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)

Note que em nosso exemplo aqui, estamos usando a mesma função _process_snapshot para processar o instantâneo. Isso faz sentido na maioria das situações. No entanto, se os componentes especificados por você ao criar o instantâneo forem diferentes entre o seu instantâneo de descoberta e o seu instantâneo de atualização, você terá que levar em consideração os diferentes componentes.

Consultando instantâneos

Assim que tivermos um instantâneo, podemos executar consultas sobre esse instantâneo para obter os dados contidos nele. É garantido que o instantâneo permanecerá inalterado até que você o libere (free).

Para cada componente que adicionamos ao nosso instantâneo (snapshot), temos um objeto de dados que o acompanha. Este objeto de dados tem uma função dupla: adicioná-lo à sua consulta garante que consultemos esse tipo de componente, e ele é o objeto no qual os dados consultados são carregados.

Existe um objeto de dados especial que deve ser sempre adicionado à nossa lista de requisições como a primeiríssima entrada, e esse é o OpenXRSpatialQueryResultData. Este objeto conterá uma entrada para cada entidade retornada com seu ID único e o estado atual da entidade.

Para completar nossa lógica de descoberta, acrescentamos o seguinte:

...

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

No exemplo acima, estamos contando com o ENTITY_TRACKING_STATE_STOPPED para limpar as entidades espaciais que não estão mais sendo rastreadas. Isso está disponível apenas com instantâneos de atualização.

Para capacidades que dependem apenas de instantâneos de descoberta, você pode preferir fazer uma limpeza baseada em entidades que não fazem mais parte do instantâneo, em vez de depender da mudança de estado.

Entidades espaciais

Com as informações acima, agora sabemos como consultar nossas entidades espaciais e obter informações sobre elas, mas há um pouco mais que precisamos analisar quando se trata das entidades em si.

Em teoria, estamos obtendo todos os nossos dados a partir dos nossos instantâneos, no entanto, o OpenXR possui uma API extra onde criamos um objeto de entidade espacial a partir do ID da nossa entidade. Enquanto este objeto existir, o runtime de XR saberá que estamos usando esta entidade e que ela não deve ser limpa precocemente. Isso é um pré-requisito para realizar uma consulta de atualização nesta entidade.

Em nosso código de exemplo, fazemos isso chamando OpenXRSpatialEntityExtension.make_spatial_entity.

Algumas APIs de entidades espaciais criarão automaticamente o objeto para nós. Nesse caso, precisamos chamar OpenXRSpatialEntityExtension.add_spatial_entity para registrar o objeto criado com nossa implementação.

Ambas as funções retornam um RID que podemos usar em funções posteriores que exijam o nosso objeto de entidade.

Quando terminarmos, podemos chamar OpenXRSpatialEntityExtension.free_spatial_entity.

Note que não fizemos isso em nosso código de exemplo. Isso é gerenciado automaticamente quando nossa instância de OpenXRSpatialEntityTracker é destruída.

Capacidade de âncora espacial (Spatial anchor)

As âncoras espaciais são gerenciadas pelo nosso objeto singleton OpenXRSpatialAnchorCapability. Após a sessão do OpenXR ter sido criada, você pode chamar OpenXRSpatialAnchorCapability.is_spatial_anchor_supported para verificar se o recurso de âncora espacial é suportado no seu hardware.

A capacidade de âncora espacial quebra um pouco o molde do que mostramos acima.

O sistema de âncoras espaciais nos permite identificar, rastrear, persistir e compartilhar uma localização física. O que torna isso diferente é que estamos criando e destruindo a âncora e, portanto, gerenciando o seu ciclo de vida.

Dessa forma, usamos apenas o sistema de descoberta para descobrir âncoras criadas e persistidas em sessões anteriores, ou âncoras compartilhadas conosco.

Nota

O compartilhamento de âncoras atualmente não é suportado na especificação de entidades espaciais.

Como mostramos em nosso exemplo anterior, sempre começamos com a criação de um contexto espacial, mas agora usando o objeto de configuração OpenXRSpatialCapabilityConfigurationAnchor. Mostraremos um exemplo deste código depois de discutirmos os escopos de persistência. Primeiro, analisaremos o gerenciamento de âncoras locais.

Não há diferença na criação de âncoras espaciais em relação ao que discutimos sobre a lógica embutida. A única coisa importante é passar o seu próprio contexto espacial como um parâmetro para OpenXRSpatialAnchorCapability.create_new_anchor.

Tornar uma âncora persistente exige que você espere até que a âncora esteja sendo rastreada, o que significa que você deve realizar consultas de atualização para qualquer âncora criada para que possa processar as mudanças de estado.

Para habilitar a persistência de âncoras, você também precisa configurar um escopo de persistência. No núcleo do OpenXR, dois tipos de escopos de persistência são suportados:

Escopos de persistência

Enum

Descrição

PERSISTENCE_SCOPE_SYSTEM_MANAGED

Fornece ao aplicativo acesso somente leitura (ou seja, os aplicativos não podem modificar este armazenamento) às entidades espaciais persistidas e gerenciadas pelo sistema. O aplicativo pode usar o UUID no componente de persistência deste armazenamento para correlacionar entidades entre contextos espaciais e reinicializações do dispositivo.

PERSISTENCE_SCOPE_LOCAL_ANCHORS

Persistence operations and data access is limited to spatial anchors, on the same device, for the same user and app (using persist_anchor() and unpersist_anchor() functions)

Começaremos com um novo script que gerencia nossas âncoras espaciais. Ele será semelhante ao script apresentado anteriormente, mas com algumas diferenças.

A primeira delas sendo a criação do nosso escopo de persistência.

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)

Com o nosso escopo de persistência criado, podemos agora criar o nosso contexto espacial.

...

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)

Criar o nosso instantâneo de descoberta para as nossas âncoras é quase o mesmo que fizemos antes, no entanto, só faz sentido criar o nosso instantâneo para âncoras persistentes. Já conhecemos as âncoras que criamos durante a nossa sessão, apenas queremos acesso àquelas vindas do runtime de XR.

Também queremos realizar consultas de atualização regulares, aqui estamos interessados apenas no estado, portanto queremos processar nosso instantâneo de forma ligeiramente diferente.

O sistema de âncoras nos dá acesso a dois componentes:

Componentes de âncora

Componente

Classe de dados

Descrição

COMPONENT_TYPE_ANCHOR

OpenXRSpatialComponentAnchorList

Fornece-nos a pose (localização + orientação) de cada âncora

COMPONENT_TYPE_PERSISTENCE

OpenXRSpatialComponentPersistenceList

Fornece-nos o estado de persistência e o UUID de cada âncora

...

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

Finalmente, podemos processar nosso instantâneo. Note que estamos usando OpenXRAnchorTracker como nossa classe de rastreador, pois ela já possui todo o suporte para âncoras embutido.

...

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)

Capacidade de rastreamento de planos (Plane tracking)

O rastreamento de planos é gerenciado pela classe singleton OpenXRSpatialPlaneTrackingCapability.

Após a sessão do OpenXR ter sido criada, você pode chamar OpenXRSpatialPlaneTrackingCapability.is_supported para verificar se o recurso de rastreamento de planos é suportado no seu hardware.

Embora tenhamos fornecido a maior parte do código para o rastreamento de planos acima, apresentaremos a implementação completa abaixo, pois ela possui alguns pequenos ajustes. Não há necessidade de atualizar os instantâneos aqui, apenas fazemos nosso instantâneo de descoberta e implementamos nossa função de processamento.

O rastreamento de planos dá acesso a dois componentes que têm suporte garantido, e três componentes opcionais.

Componentes de rastreamento de planos

Componente

Classe de dados

Descrição

COMPONENT_TYPE_BOUNDED_2D

OpenXRSpatialComponentBounded2DList

Fornece-nos a pose central e o retângulo delimitador (bounding rectangle) para cada plano.

COMPONENT_TYPE_PLANE_ALIGNMENT

OpenXRSpatialComponentPlaneAlignmentList

Fornece-nos o alinhamento de cada plano

COMPONENT_TYPE_MESH_2D

OpenXRSpatialComponentMesh2DList

Fornece-nos uma malha 2D (mesh) que molda cada plano

COMPONENT_TYPE_POLYGON_2D

OpenXRSpatialComponentPolygon2DList

Fornece-nos um polígono 2D que molda cada plano

COMPONENT_TYPE_PLANE_SEMANTIC_LABEL

OpenXRSpatialComponentPlaneSemanticLabelList

Fornece-nos uma identificação do tipo de cada plano

Nosso objeto de configuração de rastreamento de planos já habilita todos os componentes suportados, mas precisaremos interrogá-lo, então salvaremos nossa instância em uma variável de membro. Podemos usar nosso objeto rastreador OpenXRPlaneTracker para armazenar nossos dados de componentes.

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)

Capacidade de rastreamento de marcadores (Marker tracking)

O rastreamento de marcadores é gerenciado pela classe singleton OpenXRSpatialMarkerTrackingCapability.

O rastreamento de marcadores funciona de forma semelhante ao rastreamento de planos, no entanto, agora estamos rastreando entidades específicas no mundo real com base em algum código impresso em um objeto, como um pedaço de papel.

Existem várias opções diferentes de rastreamento de marcadores. O OpenXR suporta 4 nativamente; a tabela a seguir fornece mais informações e o nome da função com a qual verificar se o seu headset suporta uma determinada opção:

Opções de rastreamento de marcadores

Opção

Verificar suporte

Objeto de configuração

April tag

april_tag_is_supported

OpenXRSpatialCapabilityConfigurationAprilTag

Aruco

aruco_is_supported

OpenXRSpatialCapabilityConfigurationAruco

Código QR

qrcode_is_supported

OpenXRSpatialCapabilityConfigurationQrCode

Micro código QR

micro_qrcode_is_supported

OpenXRSpatialCapabilityConfigurationMicroQrCode

Cada opção possui seu próprio objeto de configuração que você pode usar ao criar uma entidade espacial.

Os códigos QR permitem que você codifique uma string que é decodificada pelo runtime de XR e fica acessível quando um marcador é encontrado. Com as tags April e marcadores Aruco, dados binários são codificados e você também pode acessá-los quando um marcador é encontrado, no entanto, você precisa configurar a detecção com o formato de decodificação correto.

Como exemplo, criaremos um contexto espacial que encontrará códigos QR e marcadores 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)

Cada marcador, independentemente do tipo, consistirá em dois componentes:

Componentes de rastreamento de marcadores

Componente

Classe de dados

Descrição

COMPONENT_TYPE_MARKER

OpenXRSpatialComponentMarkerList

Fornece-nos o tipo, ID (Aruco e AprilTag) e/ou os dados (Código QR) para cada marcador.

COMPONENT_TYPE_BOUNDED_2D

OpenXRSpatialComponentBounded2DList

Fornece-nos a pose central e o retângulo delimitador (bounding rectangle) para cada plano.

Adicionamos nossa implementação de descoberta:

...

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 adicionamos nossa funcionalidade de atualização:

...


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)