SpatialMap_SparseSpatialMap

演示如何使用稀疏空间地图。

  • 演示如何创建稀疏空间地图

  • 演示如何在稀疏空间地图上摆放虚拟物体

  • 演示如何预览持久化的内容

  • 演示如何定位多张地图并展示上面的内容

参考: 运动跟踪与EasyAR功能

运行前配置

使用稀疏空间地图需配置服务器访问信息,这些信息可以在EasyAR官网的开发中心中 SpatialMap页面 中获得。在Unity中输入这些信息有两种方法:

一种是全局配置,所有使用全局配置的稀疏空间地图场景都会使用这个配置。从Unity菜单中选择<EasyAR -> Sense -> Configuration>

../../_images/image_62.png

然后在Project Settings中输入从开发中心获取的信息。

../../_images/image_62_1.png

另一种是在场景中配置,它只对当前场景有效。

../../_images/image_62_2.png

用法

Main视图

../../_images/image_s14_1.png
标记 1: 进入create 视图,可以创建spatial map。
标记 2: 进入edit 视图,可以将3D内容摆放到spatial map上并保存场景。
标记 3: 进入preview 视图,可以预览在edit 视图中添加到map上的内容。
标记 4: Map列表,选择的map可以用来编辑和预览。
标记 5: 删除选择的map。
标记 6: 删除所有map及缓存。它的行为可以由 ViewManager.MainViewRecycleBinClearMapCacheOnly 控制。
../../_images/image_s14_2.png ../../_images/image_s14_3.png

点击屏幕黑色区域可以收缩和展开map列表。

../../_images/image_s14_4.png ../../_images/image_s14_5.png

map列表初始是空的,会在创建之后逐渐增加。

选择列表中的一张map,然后点击Edit可以放置3D内容,点击Preview可以预览放置的内容。

选择列表中多张map,只有预览可以点击。在这个sample中,所有选择的map都会被检测和跟踪。

Create视图

../../_images/image_s14_6.png ../../_images/image_s14_7.png
标记 1: 打开/关闭操作提示。
标记 2: 打开用来保存map的弹窗。
标记 3: 返回main 视图。
标记 4: Map 名字,它会自动填写,并在上传前可修改。
标记 5: 截取当前显示作为预览图。预览图会在点击save按钮的时候自动创建,在上传前可以使用这个按钮修改。
标记 6: 控制是否在上传map的同时上传预览图。整个区域都可以被点击来改变选择。弹窗的标题会根据选择而改变。如果你不希望截图被上传,点击直至复选框取消选中。
标记 7: 上传并生成地图。上传会花一些时间,弹窗会在上传成功后消失并返回main 视图。如果上传失败,可以选择重试或取消。
标记 8: 取消上传并返回main 视图。

Edit视图

../../_images/image_s14_8.png
标记 1: 切换视频是否可以播放。如果选中,视频可以在被选中的时候播放。
标记 2: 切换自由移动。如果选择,可以将物体移动到3D空间中的任何位置,否则会贴着点云摆放。
标记 3: 打开/关闭操作提示。
标记 4: 模板列表,保存可以被实例化的物体模板。
标记 5: 模板图标。可以拖动这个图标到map点云上,图标会在3D空间中变成实际的3D物体。
标记 6: 删除场景中选择的物体。
标记 7: 保存当前场景。在保存后,下次使用同一张map进入edit 视图或preview 视图,所有内容会恢复到保存时所放置的位置。
标记 8: 返回main 视图。
标记 9: 切换点云显示。这个按钮会在地图定位到之后显示。
../../_images/image_s14_9.png ../../_images/image_s14_10.png

模板图标可以被拖到场景中。如果当前有map被定位到,拖动图标到点云上会在3D场景中生成物体。可以拖动多次并生成多个物体。

预设的四个按钮的内容为:立方体、名片图片、红包模型和视频。

Preview视图

../../_images/image_s14_11.png
标记 1: 返回main 视图。
标记 2: 切换点云显示。这个按钮会在地图定位到之后显示。

详解

Active ARSession

在这个sample中,启动时场景中ARSession的active是false。

../../_images/image_s14_12.png

它会在每次从main视图进入子视图的时候被复制一份。sample中定义的MapSession的一个实例也会被同时创建来处理与ARSession相关联的数据。

easyarObject = Instantiate(EasyARSession);
easyarObject.SetActive(true);
session = easyarObject.GetComponent<ARSession>();
mapFrameFilter = easyarObject.GetComponentInChildren<SparseSpatialMapWorkerFrameFilter>();
mapSession = new MapSession(session, mapFrameFilter, selectedMaps);

设置用来构建的map

当create视图将打开的时候,会创建一个用来在视图中处理建图数据的map。它从 SparseSpatialMap prefab实例化, SparseSpatialMapController.SourceType 设为 SparseSpatialMapController.DataSource.MapBuilderSparseSpatialMapController.MapWorker 设为session中的 SparseSpatialMapWorkerFrameFilter 。为使建图过程可视化 SparseSpatialMapController.ShowPointCloud 设成true。

public void SetupMapBuilder(SparseSpatialMapController mapControllerPrefab)
{
    builderMapController = UnityEngine.Object.Instantiate(mapControllerPrefab);
    builderMapController.SourceType = SparseSpatialMapController.DataSource.MapBuilder;
    builderMapController.MapWorker = MapWorker;
    builderMapController.ShowPointCloud = true;
}

创建预览图

当保存map的时候,预览图可以被同时上传。从camera渲染的内容创建图像很简单。使用texture创建 Buffer 然后使用这个buffer来创建图像。不要忘记在操作完成后 Dispose 底层数据或是使用 using 语义来完成。

using (var buffer = easyar.Buffer.wrapByteArray(capturedImage.GetRawTextureData()))
using (var image = new easyar.Image(buffer, PixelFormat.RGB888, capturedImage.width, capturedImage.height))
{
    mapSession.Save(mapName, withPreview ? image : null);
}

保存map

MapWorker.BuilderMapController.Host 可以用来保存 map。这个调用可能会抛出异常。host过程可能会花一些时间,因此这里使用 SparseSpatialMapWorkerFrameFilter.BuilderMapController.MapHost 来异步获取处理结果。

public void Save(string name, Optional<Image> preview)
{
    IsSaving = true;
    MapWorker.BuilderMapController.MapHost += (map, isSuccessful, error) =>
    {
        ...
        IsSaving = false;
    };
    try
    {
        MapWorker.BuilderMapController.Host(name, preview);
    }
    catch (Exception e)
    {
        ...
        IsSaving = false;
    }
}

注意:map名字可以在网站上修改,不过SDK端在缓存清除并重新下载之前不会改变。

在map成功保存后,sample还保存了一些本地数据。这个数据记录了从server访问map访问信息,以确保后续的编辑或预览可以从server获取和使用map。

MapWorker.BuilderMapController.MapHost += (map, isSuccessful, error) =>
{
    if (isSuccessful)
    {
        var mapMeta = new MapMeta(map, new List<MapMeta.PropInfo>());
        MapMetaManager.Save(mapMeta);
        ...
    }
    ...
};

设置用来定位的map

当edit视图或preview视图将打开的时候,一部分map会被加载并用来定位。它们会从server加载,因此 SparseSpatialMapController.SourceType 设为 SparseSpatialMapController.DataSource.MapManagerSparseSpatialMapController.MapManagerSource 设为之前保存map时同时保存的本地数据, SparseSpatialMapController.MapWorker 设备session中的 SparseSpatialMapWorkerFrameFilter

在edit视图中,map默认设为可见, SparseSpatialMapController.ShowPointCloud 设成 true,而在preview视图中会反过来。这个数值可以在运行时通过 Point Cloud 按钮实时改变。

public void LoadMapMeta(SparseSpatialMapController mapControllerPrefab, bool particleShow)
{
    ...
    foreach (var m in Maps)
    {
        var meta = m.Meta;
        var controller = UnityEngine.Object.Instantiate(mapControllerPrefab);
        controller.SourceType = SparseSpatialMapController.DataSource.MapManager;
        controller.MapManagerSource = meta.Map;
        controller.MapWorker = MapWorker;
        controller.ShowPointCloud = particleShow;
        ...
        m.Controller = controller;
    }
}

启动map定位

定位会在 SparseSpatialMapWorkerFrameFilter.Localizer.startLocalization 调用的时候开始。默认情况下,定位一旦成功就会停止。

public void LoadMapMeta(SparseSpatialMapController mapControllerPrefab, bool particleShow)
{
    ...
    MapWorker.Localizer.startLocalization();
}

设置多map定位

如果要定位场景中多张map,定位功能不能停止。因此在选择了多张map的时候 SparseSpatialMapWorkerFrameFilter.LocalizerConfig.LocalizationMode 设为 LocalizationMode.KeepUpdate 。这会在接下来的 SparseSpatialMapWorkerFrameFilter.StartSparseSpatialMapWorkerFrameFilter.OnEnable 调用后生效。在这个sample中,根据调用的时序这是可以被保证的。

if (Maps.Count > 1)
{
    MapWorker.LocalizerConfig.LocalizationMode = LocalizationMode.KeepUpdate;
}

定位多张map时当前所定位的map

为了使定位切换可见,map的颜色被设为会在定位到和停止定位时改变。

controller.MapLocalized += () =>
{
    GUIPopup.EnqueueMessage("Localized map {name = " + controller.MapInfo.Name + "}", 3);
    var parameter = controller.PointCloudParticleParameter;
    parameter.StartColor = new Color32(11, 205, 255, 255);
    controller.PointCloudParticleParameter = parameter;
};
controller.MapStopLocalize += () =>
{
    GUIPopup.EnqueueMessage("Stopped localize map {name = " + controller.MapInfo.Name + "}", 3);
    var parameter = controller.PointCloudParticleParameter;
    parameter.StartColor = new Color32(163, 236, 255, 255);
    controller.PointCloudParticleParameter = parameter;
};

Prop集合

这个sample定义了 PropCollection 类来处理可以被拖动并在场景中实例化的模板。

public class PropCollection : MonoBehaviour
{
    ...
    public List<Templet> Templets = new List<Templet>();
    ...
    [Serializable]
    public class Templet
    {
        public GameObject Object;
        public Sprite Icon;
    }
}

如果你想在深入理解这个sample前尝试放置其它物体,只需要在Inspector中添加你所期望的物体的prefab和显示在底部工具条上的图标即可。注意物体的名字不要重复。

../../_images/image_s14_13.png

在点云上移动物体

可以对sparse spatial map执行 hit test 并移动物体到返回的点。为使物体跟踪map,需要将这个点变换到map本地坐标。

var point = mapSession.HitTestOne(new Vector2(Input.touches[0].position.x / Screen.width, Input.touches[0].position.y / Screen.height));

public Optional<Vector3> HitTestOne(Vector2 pointInView)
{
    ...
    foreach (var point in MapWorker.LocalizedMap.HitTest(pointInView))
    {
        return MapWorker.LocalizedMap.transform.TransformPoint(point);
    }
    ...
}

物体的位置会稍作调整,这样它可以正好显示在点云上方。

candidate.transform.position = point.Value + Vector3.up * candidate.transform.localScale.y / 2;

Map meta

这个sample定义了 MapMeta 类来处理map上放置的所有物体,用以一起保存和加载。当下次使用同一张map打开这个视图的时候,物体可以保持在原位。它会保存map本身用户从服务器访问map,也会保存所有物体相对map的变换关系。

这个sample在本地设备上保存这些数据,如果你将这些数据存储在网络服务器上,就可以完成地图和内容的分享。

[Serializable]
public class MapMeta
{
    public SparseSpatialMapController.MapManagerSourceData Map = new SparseSpatialMapController.MapManagerSourceData();
    public List<PropInfo> Props = new List<PropInfo>();
    ...
    [Serializable]
    public class PropInfo
    {
        public string Name = string.Empty;
        public float[] Position = new float[3];
        public float[] Rotation = new float[4];
        public float[] Scale = new float[3];
    }
}

保存map meta

在edit视图点击 Save 按钮,map meta会被保存。这个过程会更新所有物体的transform并将它们存储到文件。

public void Save()
{
    if (mapData == null)
    {
        return;
    }

    var propInfos = new List<MapMeta.PropInfo>();

    foreach (var prop in mapData.Props)
    {
        var position = prop.transform.localPosition;
        var rotation = prop.transform.localRotation;
        var scale = prop.transform.localScale;

        propInfos.Add(new MapMeta.PropInfo()
        {
            Name = prop.name,
            Position = new float[3] { position.x, position.y, position.z },
            Rotation = new float[4] { rotation.x, rotation.y, rotation.z, rotation.w },
            Scale = new float[3] { scale.x, scale.y, scale.z }
        });
    }
    mapData.Meta.Props = propInfos;

    MapMetaManager.Save(mapData.Meta);
}

加载map meta

在每次进入edit视图或preview视图时,map meta会在map加载的 SparseSpatialMapController.MapLoad 事件处理中加载。物体会根据模板名字来创建,map是它们的父节点。相对map的transform也会从meta data中被恢复。

controller.MapLoad += (map, status, error) =>
{
    ...
    foreach (var propInfo in meta.Props)
    {
        GameObject prop = null;
        foreach (var templet in PropCollection.Instance.Templets)
        {
            if (templet.Object.name == propInfo.Name)
            {
                prop = UnityEngine.Object.Instantiate(templet.Object);
                break;
            }
        }
        if (!prop)
        {
            Debug.LogError("Missing prop templet: " + propInfo.Name);
            continue;
        }
        prop.transform.parent = controller.transform;
        prop.transform.localPosition = new UnityEngine.Vector3(propInfo.Position[0], propInfo.Position[1], propInfo.Position[2]);
        prop.transform.localRotation = new Quaternion(propInfo.Rotation[0], propInfo.Rotation[1], propInfo.Rotation[2], propInfo.Rotation[3]);
        prop.transform.localScale = new UnityEngine.Vector3(propInfo.Scale[0], propInfo.Scale[1], propInfo.Scale[2]);
        prop.name = propInfo.Name;
        m.Props.Add(prop);
    }
};

Map缓存

Sparse spatial map会使用设备缓存来保存下载的map。可以通过 SparseSpatialMapManager.clear 来清除缓存。

public static void ClearCache()
{
    if (SparseSpatialMapManager.isAvailable())
    {
        using (var manager = SparseSpatialMapManager.create())
        {
            manager.clear();
        }
    }
}

在这个sample中,ViewManager里有一个隐藏的选项 ViewManager.MainViewRecycleBinClearMapCacheOnly 选项,可以控制clear的行为用来测试。

public void ClearAll()
{
    ...
    if (!ViewManager.Instance.MainViewRecycleBinClearMapCacheOnly)
    {
        // clear map meta and the list on UI
        foreach (var cell in cells)
        {
            if (cell)
            {
                MapMetaManager.Delete(cell.MapMeta);
                Destroy(cell.gameObject);
            }
        }
        cells.Clear();
    }

    // clear map cache
    MapSession.ClearCache();
    ...
}

默认情况下, ViewManager.MainViewRecycleBinClearMapCacheOnly 是 false,也就是说,main视图的回收站图标会清楚map缓存以及map列表。在这种情况下,加载地图不会触发下载(缓存在上传时已经建立)。在进入edit视图或preview视图时加载map的过程中,服务器上的请求次数统计数据将不会增加。

可以修改 ViewManager.MainViewRecycleBinClearMapCacheOnly 为 true 来测试真实情况下map通过map id分享的行为。在这种情况下,clear之后(每个map的第一次)加载map会触发下载。在进入edit视图或preview视图时加载map的过程中,服务器上的请求次数统计数据会增加。在成功的下载后,map缓存会被使用。它会在 SparseSpatialMapManager.clear 调用或应用卸载时被清除。如果你分发应用并让不同用户访问同一张map的时候就会是这样的行为。

分享Map

在这个sample中,你可以创建和上传map,但map分享并为展示。通常来说这并不复杂,你只需要知道需要分析什么信息就可以。一般来说,你需要将 SparseSpatialMapManager 所需的服务器的访问信息(API key、API secret和 sparse spatial map 服务AppID)以及map的信息(只有map ID是必须的)写进应用。当然你也可以参考这个sample中的map meta来做内容分享以及如何恢复map上的内容。在另一个设备上做与在同一个设备上做并没有什么区别。

另外你也可以参考 SpatialMap_Sparse_Localizing sample。它会使用上面提到的信息作为输入。你可以使用这个sample创建的一个map来设置 SpatialMap_Sparse_Localizing sample。它将可以定位这个sample创建的map,即使这两个sample并没有安装在同一个设备上。