Combining Multiple Area Targets

Multiple Area Targets made from individual scans can be placed together in the Unity Editor to create a seamless tracking experience of multiple connected spaces.

It is possible to scan multiple rooms and areas separately and then fit them together later to cover a single AR experience over a larger area. However, the Area Targets might drift away from one another at runtime; leaving a gap or incorrect poses of the individual Area Targets.

Incorrect poses of the Area Targets can be seen when only one of multiple Area Targets is in view. The one in view (EXTENDED_TRACKED), will have a correct pose whilst the others have no means of adjusting theirs since they are not directly tracked (LIMITED).

The solution is therefore to constrain the Area Targets to one another and provide each a common pose as if one would track the combination of targets as one connected space.

NOTE: In order to add navigation to this type of setup with multiple Area Targets, you can use Unity's Runtime NavMesh Generation instead of the Vuforia NavMesh guide that applies NavMesh to a single Area Target. 

Setup Unity Project

To follow this guide, ensure that the latest supported Unity version and the latest Vuforia Engine SDK is correctly set up. For a guide to setting up the Vuforia Engine and importing databases, please see our Unity Guide.

  • Import two or more Area Targets that can be fitted together as they are in reality.

Scene Composition

Below is an illustrative example of how three Area Targets were positioned together to form an apartment. Perform the same operation with your Area Targets using the Rotation and Positioning tool in the Unity scene

  1. Position your Area Targets according to the physical environment.
  2. Create an Empty GameObject and name it MultiArea.
    1. Set the position of the MultiArea to (0, 0, 0).
  3. Drop the Area Targets as child of MultiArea in the Hierarchy.

  1. Add augmentations normally: As children of the Area Targets. 

MultiArea

To fix the Area Targets together, we need to work with the poses of the targets, ranking them to only track the most reliable pose which most likely is the area users are situated in at that given time. The below script does the following:

  • Saves the relative pose of each Area Target at start of runtime.
  • At each frame, it queries the Vuforia State list for active targets to check their tracking status, EXTENDED_TRACKED or LIMITED.
  • Based on the returned tracking status, it ranks and select the most reliable target pose.
    • If two or more targets are ranked equally, any of them will be used; they will usually be consistent with one another.
  • The script then returns the pose of the reliable target to the MultiArea pose and updates its pose, and consequently, all the Area Targets and child GameObjects.

Add the script to the MultiArea GameObject.

  1. In a project folder, create a new script with the name MultiArea.cs.
  2. Open the empty script and copy the below code snippet.
  3. Save the file.
  4. Select the MultiArea GameObject and press Add Component in the Inspector.
  5. Select the MultiArea script.
/*==============================================================================
Copyright (c) 2021, PTC Inc. All rights reserved.
Vuforia is a trademark of PTC Inc., registered in the United States and other countries.
==============================================================================*/
using System.Collections.Generic;
using UnityEngine;
using Vuforia;

public class MultiArea : MonoBehaviour
{
    #region PUBLIC_MEMBER_VARIABLES

    public bool hideAugmentationsWhenNotTracked = true;

    #endregion PUBLIC_MEMBER_VARIABLES



    #region PRIVATE_MEMBER_VARS

    /// <summary>
    /// Trackable poses relative to the MultiArea root
    /// </summary>
    private readonly Dictionary<string, Matrix4x4> mPoses = new Dictionary<string, Matrix4x4>();
    private bool m_Tracked = false;

    #endregion PRIVATE_MEMBER_VARS



    #region UNITY_MONOBEHAVIOUR_METHODS

    // Start is called before the first frame update
    void Start()
    {
        var areaTargets = GetComponentsInChildren<AreaTargetBehaviour>(includeInactive: true);
        foreach (var at in areaTargets)
        {
            // Remember the relative pose of each AT to the group root node
            var matrix = GetFromToMatrix(at.transform, transform);
            mPoses[at.TrackableName] = matrix;
            Debug.Log("Original pose: " + at.TrackableName + "\n" + matrix.ToString(""));

            // Detach augmentation and re-parent it under the group root node
            for (int i = at.transform.childCount - 1; i >= 0; i--)
            {
                var child = at.transform.GetChild(i);
                child.SetParent(transform, worldPositionStays: true);
            }

            if (hideAugmentationsWhenNotTracked)
            {
                ShowAugmentations(false);
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (!VuforiaARController.Instance.HasStarted)
        {
            return;
        }

        // Find current "best tracked" Area Target
        var atb = GetBestTrackedAreaTarget();
        if (!atb)
        {
            if (m_Tracked)
            {
                m_Tracked = false;
                if (hideAugmentationsWhenNotTracked)
                {
                    ShowAugmentations(false);
                }
            }
            return;
        }

        if (!m_Tracked)
        {
            m_Tracked = true;
            ShowAugmentations(true);
        }
        
        if (GetGroupPoseFromAreaTarget(atb, out Matrix4x4 groupPose))
        {
            // set new group pose
            transform.position = groupPose.GetColumn(3);
            transform.rotation = Quaternion.LookRotation(groupPose.GetColumn(2), groupPose.GetColumn(1));
        }
    }

    #endregion UNITY_MONOBEHAVIOUR_METHODS



    #region PRIVATE_METHODS

    private void ShowAugmentations(bool show)
    {
        var renderers = GetComponentsInChildren<Renderer>();
        foreach (var rnd in renderers)
        {
            rnd.enabled = show;
        }
    }

    private AreaTargetBehaviour GetBestTrackedAreaTarget()
    {
        var trackedAreaTargets = GetTrackedAreaTargets(includeLimited: true);
        if (trackedAreaTargets.Count == 0)
        {
            return null;
        }

        // look for extended/tracked targets
        foreach (var tb in trackedAreaTargets)
        {
            if (tb.CurrentStatus == TrackableBehaviour.Status.TRACKED ||
                tb.CurrentStatus == TrackableBehaviour.Status.EXTENDED_TRACKED)
            {
                return tb;
            }
        }

        // if no target in EXT/TRACKED was found,
        // then fallback to any other target
        // i.e. including LIMITED ones;
        // just report the first in the list
        return trackedAreaTargets[0];
    }

    private List<AreaTargetBehaviour> GetTrackedAreaTargets(bool includeLimited = false)
    {
        List<AreaTargetBehaviour> trackedTargets = new List<AreaTargetBehaviour>();
        StateManager sm = TrackerManager.Instance.GetStateManager();
        var activeTrackables = sm.GetActiveTrackableBehaviours();
        foreach (var tb in activeTrackables)
        {
            if (!(tb is AreaTargetBehaviour))
                continue;//skip non-area-targets

            if (tb.CurrentStatus == TrackableBehaviour.Status.TRACKED ||
                tb.CurrentStatus == TrackableBehaviour.Status.EXTENDED_TRACKED ||
                (includeLimited && tb.CurrentStatus == TrackableBehaviour.Status.LIMITED))
            {
                trackedTargets.Add(tb as AreaTargetBehaviour);
            }
        }
        return trackedTargets;
    }

    private bool GetGroupPoseFromAreaTarget(AreaTargetBehaviour atb, out Matrix4x4 groupPose)
    {
        groupPose = Matrix4x4.identity;
        if (mPoses.TryGetValue(atb.TrackableName, out Matrix4x4 areaTargetToGroup))
        {
            // Matrix of group root node w.r.t. AT
            var groupToAreaTarget = areaTargetToGroup.inverse;

            // Current atb matrix
            var areaTargetToWorld = atb.transform.localToWorldMatrix;
            groupPose = areaTargetToWorld * groupToAreaTarget;
            return true;
        }
        return false;
    }

    private static Matrix4x4 GetFromToMatrix(Transform from, Transform to)
    {
        var m1 = from ? from.localToWorldMatrix : Matrix4x4.identity;
        var m2 = to ? to.worldToLocalMatrix : Matrix4x4.identity;
        return m2 * m1;
    }

    #endregion PRIVATE_METHODS
}