SpatialMap_SparseSpatialMap¶
演示如何使用稀疏空间地图。
演示如何创建稀疏空间地图
演示如何在稀疏空间地图上摆放虚拟物体
演示如何预览持久化的内容
演示如何定位多张地图并展示上面的内容
参考: 运动跟踪与EasyAR功能 。
运行前配置¶
使用稀疏空间地图需配置服务器访问信息,这些信息可以在EasyAR官网的开发中心中 SpatialMap页面 中获得。在Unity中输入这些信息有两种方法:
一种是全局配置,所有使用全局配置的稀疏空间地图场景都会使用这个配置。从Unity菜单中选择<EasyAR -> Sense -> Configuration>
然后在Project Settings中输入从开发中心获取的信息。
另一种是在场景中配置,它只对当前场景有效。
用法¶
Main视图¶
点击屏幕黑色区域可以收缩和展开map列表。
map列表初始是空的,会在创建之后逐渐增加。
选择列表中的一张map,然后点击Edit可以放置3D内容,点击Preview可以预览放置的内容。
选择列表中多张map,只有预览可以点击。在这个sample中,所有选择的map都会被检测和跟踪。
Create视图¶
Edit视图¶
模板图标可以被拖到场景中。如果当前有map被定位到,拖动图标到点云上会在3D场景中生成物体。可以拖动多次并生成多个物体。
预设的四个按钮的内容为:立方体、名片图片、红包模型和视频。
Preview视图¶
详解¶
Active ARSession¶
在这个sample中,启动时场景中ARSession的active是false。
它会在每次从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.MapBuilder , SparseSpatialMapController.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.MapManager 。 SparseSpatialMapController.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.Start 或 SparseSpatialMapWorkerFrameFilter.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和显示在底部工具条上的图标即可。注意物体的名字不要重复。
在点云上移动物体¶
可以对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并没有安装在同一个设备上。