本帖主要描写编辑场景的功能实现,以及一些需要注意的问题。跟上层贴有所关联,想要更多了解请移步链接。
上一篇写的 编辑场景 帖子太细了,觉得没有必要。之后主要描述代码。
其实编辑功能主要是将 从easyAR服务器下来下来的,之前上传的 点云信息,保存在本地,然后再在Unity中加载本地的点云信息,从而在场景中进行编辑。
创建 EditeMapController.cs 本代码主要做的就是从服务器下载点云,保存本地(这一步在手机端操作),在Unity中对场景进行编辑。(这一步在Unity中编辑)
首先讲一下空间地图和摆放的物体之间有啥联系,为啥我在场景中放置一个鸭子,在手机识别后就可以在这个地方看到这只鸭子。?
EasyAR在扫描场景之后会生成两个东西。
一:就是点云信息。点云就是依据空间位置生成的点。
二:就是meta信息。在扫描识别出场景后,就会加载meta 信息,再根据meta信息加载出对应的游戏物体。meta就是依据这些点云保存的对象信息,例如鸭子的名字,旋转,位置,缩放等信息。
场景配置:
接下来是代码部分:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Common;
using easyar;
using SpatialMap_SparseSpatialMap;
using UnityEngine;
using UnityEngine.Android;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class EditeMapController : MonoBehaviour
{
public SparseSpatialMapController mapTemp;
private SparseSpatialMapWorkerFrameFilter mapWorker;
private MapSession mapSession;
private ARSession session;
public Dragger PropDragger;
private VideoCameraDevice videoCamera;
[SerializeField]
private MapSession.MapData mapData;
private void Awake()
{
session = FindObjectOfType<ARSession>();
mapWorker = FindObjectOfType<SparseSpatialMapWorkerFrameFilter>();
videoCamera = session.GetComponentInChildren<VideoCameraDevice>();
#if UNITY_EDITOR
GameObject.Find("EasyAR_SparseSpatialMapWorker").SetActive(false);
#endif
PropDragger.CreateObject += (gameObj) =>
{
if (gameObj)
{
gameObj.transform.parent = mapData.Controller.transform;
mapData.Props.Add(gameObj);
}
};
PropDragger.DeleteObject += (gameObj) =>
{
if (gameObj)
{
mapData.Props.Remove(gameObj);
}
};
}
private void Start()
{
mapSession = new MapSession(mapWorker, MapMetaManager.LoadAll());
mapSession.LoadMapMeta(mapTemp, true);
mapSession.CurrentMapLocalized = (mapData) =>
{
this.mapData = mapData;
};
videoCamera.DeviceOpened += () =>
{
if (videoCamera == null)
{
return;
}
videoCamera.FocusMode = CameraDeviceFocusMode.Continousauto;
};
#if UNITY_EDITOR
dataDropdown.gameObject.SetActive(true);
InitPointData();
#endif
PropDragger.SetMapSession(mapSession);
}
#if UNITY_EDITOR
[HideInInspector]
public List<PointData> pointDatas = new List<PointData>();
GameObject controller;
public Dropdown dataDropdown;
List<Dropdown.OptionData> OptionDataList = new List<Dropdown.OptionData>();
public void InitPointData()
{
pointDatas = MapMetaManager.Load_PointCloud<PointData>();
foreach (var item in pointDatas)
{
OptionDataList.Add(new Dropdown.OptionData() { text = item.mapName });
}
dataDropdown.options = OptionDataList;
dataDropdown.onValueChanged.AddListener(OnDropDownChanged);
DebugObj(0);
}
public void OnDropDownChanged(int index)
{
DebugObj(index);
}
public void DebugObj(int index)
{
mapData = mapSession?.Maps[index];
UpdatePointCloud(GetCurrentPointData);
controller = GameObject.Find("ObjParents");
if (controller == null)
{
controller = new GameObject("ObjParents");
}
for (int i = 0; i < controller.transform.childCount; i++)
{
Destroy(controller.transform.GetChild(i).gameObject);
}
foreach (var propInfo in mapData?.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;
mapData?.Props.Add(prop);
}
}
public ParticleSystem PointCloudParticleSystem;
SparseSpatialMapController.ParticleParameter pointCloudParticleParameter = new SparseSpatialMapController.ParticleParameter() { StartSize = 0.02f };
private void UpdatePointCloud(PointData PointData)
{
if (string.IsNullOrEmpty(PointData.mapName))
{
PointCloudParticleSystem.Clear();
return;
}
if (!PointCloudParticleSystem)
{
return;
}
var particles = PointData.PointCloud.Select(p =>
{
var particle = new ParticleSystem.Particle();
particle.position = p;
particle.startLifetime = pointCloudParticleParameter.StartLifetime;
particle.remainingLifetime = pointCloudParticleParameter.RemainingLifetime;
particle.startSize = pointCloudParticleParameter.StartSize;
particle.startColor = pointCloudParticleParameter.StartColor;
return particle;
}).ToArray();
PointCloudParticleSystem.SetParticles(particles, particles.Length);
}
private PointData GetCurrentPointData
{
get
{
return pointDatas.Find(a => a.mapName == mapData.Meta.Map.Name);
}
}
#endif
public void SavePoint()
{
MapMetaManager.Save(new PointData() { mapName = mapData.Meta.Map.Name, PointCloud = mapData.Controller.PointCloud });
}
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, MapMetaManager.FileNameType.Name);
Debug.Log("保存成功");
}
public void BackMain()
{
SceneManager.LoadScene("MainMapScene");
}
private void DestroySession()
{
if (mapSession != null)
{
mapSession.Dispose();
mapSession = null;
}
}
private void OnDestroy()
{
DestroySession();
}
}
public struct PointData
{
public string mapName;
public List<Vector3> PointCloud;
}
其中:
private MapSession mapSession;
这个MapSession其实就是个地图管理器,在Start 中对其就行初始化。
mapSession = new MapSession(mapWorker, MapMetaManager.LoadAll());
mapSession.LoadMapMeta(mapTemp, true);
mapSession.CurrentMapLocalized = (mapData) =>
{
this.mapData = mapData;
};
这个 loadAll 参见上一篇 MapMetaManager 代码。是对meta信息进行加载保存管理的脚本。返回一个 List<MapMeta> 列表。
MapMetaManager.LoadAll()
注册的回调 CurrentMapLocalized,会在当前识别到一个场景时,返回该场景的mapData。
mapSession.CurrentMapLocalized = (mapData) =>
{
this.mapData = mapData;
};
a.接下来我们看看mapData里有啥。来自类型 MapSession.MapData 。
public class MapData
{
public MapMeta Meta;
public SparseSpatialMapController Controller;
public List<GameObject> Props = new List<GameObject>();
}
注意不要混淆 MapData 和 MapMeta 的概念。MapData 是该地图的数据信息。包含了该地图的的Meta信息,controller,和Prop。Prop就是这个地图中所包含的游戏对象预制体。
b.接下来我们看看Meta 里有啥。来自 MapMeta 类型。
using easyar;
using System;
using System.Collections.Generic;
namespace SpatialMap_SparseSpatialMap
{
[Serializable]
public class MapMeta
{
public SparseSpatialMapController.MapManagerSourceData Map = new SparseSpatialMapController.MapManagerSourceData();
public List<PropInfo> Props = new List<PropInfo>();
public MapMeta(SparseSpatialMapController.SparseSpatialMapInfo map, List<PropInfo> props)
{
Map = new SparseSpatialMapController.MapManagerSourceData() { Name = map.Name, ID = map.ID };
Props = props;
}
[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];
}
}
}
mepMeta 是easyAR自带的脚本。我们只是探究一下下,不要手贱乱改。里边有List<PropInfo> 在场景加载结束后,会从 遍历 List<PropInfo> ,根据名字去场景中 PropCollection 中查询,找到后 Instantiate 生成复制体到场景。
c.其中有一个内部类。PropInfo。保存了一个字段,大概是游戏对象的 名字,缩放,位置,旋转。bingo!!!找到了。
还有一个map。来自SparseSpatialMapController.MapManagerSourceData 类型。里边存了 Name 和 ID
好了 回到我们自己的脚本。
public void SavePoint()
{
MapMetaManager.Save(new PointData() { mapName = mapData.Meta.Map.Name, PointCloud = mapData.Controller.PointCloud });
}
public struct PointData
{
public string mapName;
public List<Vector3> PointCloud;
}
这里有个 pointData自定义结构。保存了地图名字和一些点,这些点就是截取的点云信息。将点的信息保存在这里。通过 MapMetaManager.Save() 方法存到本地。接着我们看下 mapData.Controller.PointCloud 这个东西。
可以看到里边是一个List<Vector3> 的列表。这就是保存所有点的信息的地方。位于 SparseSpatialMapController.cs 脚本。
public SparseSpatialMapController mapTemp;
每一个地图会对应一个 SparseSpatialMapController 。
第一部分:保存!手机端。
UI上有两个保存按钮。
1.保存meta文件。
点击后会保存meta信息。保存ID,Name,PropInfos 路径在: Android/data/com.xxx.xxx/files/SparseSpatialMap/xxxx.meta 文件。 xxxx就是当前load并定位的地图的名字。也是扫描提交场景时起的名字。
2.保存点云数据。
保存结束后,会在手机本地路径 Android/data/com.xxx.xxx/files/SparseSpatialMap/xxxx_PointCloud.txt 文件。xxxx就是当前load并定位的地图的名字。也是扫描提交场景时起的名字。
现在我们再重新明确一下 路径。
private static readonly string root = Application.persistentDataPath + "/SparseSpatialMap";
安卓 | Android/data/com.XXXX.YYYY/files/SparseSpatialMap/ |
windows | C:\Users\Administrator\AppData\LocalLow\XXXX\YYYY\SparseSpatialMap |
其中 Administrator 是电脑的用户名, XXXX\YYYY 就是 这个,根据自己的设置而不同。
接下来我们找到安卓路径下,复制刚才手机上保存的 *.meta 和 *.txt 文件,然后粘贴在 windows的路径下,如果没有的话,就创建一个,一般当项目在Play一次之后,会自动创建的。
第二部分:调试!Unity端
SceneMaster 下有一个PointCloudParticleSystem ,这是个粒子系统,用来模拟点云,点云数据中的点的信息会控制粒子特效中的每一个粒子的位置。
在Start中,初始化 dropdown 组件
public void InitPointData()
{
pointDatas = MapMetaManager.Load_PointCloud<PointData>();
foreach (var item in pointDatas)
{
OptionDataList.Add(new Dropdown.OptionData() { text = item.mapName });
}
dataDropdown.options = OptionDataList;
dataDropdown.onValueChanged.AddListener(OnDropDownChanged);
DebugObj(0);
}
dropdown的填充物是一个个地图信息。点击不同的选项,会调用 DebugObj(index) ,加载对应的地图信息,包括 点云+prop信息。
public void OnDropDownChanged(int index)
{
DebugObj(index);
}
public void DebugObj(int index)
{
mapData = mapSession?.Maps[index];
UpdatePointCloud(GetCurrentPointData);
controller = GameObject.Find("ObjParents");
if (controller == null)
{
controller = new GameObject("ObjParents");
}
for (int i = 0; i < controller.transform.childCount; i++)
{
Destroy(controller.transform.GetChild(i).gameObject);
}
foreach (var propInfo in mapData?.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;
mapData?.Props.Add(prop);
}
}
其中 有个方法 UpdatePointCloud(GetCurrentPointData); 这个方法将点云信息中的点,赋值给粒子系统中的每一个粒子,这样就用 粒子渲染 出了一个 空间信息。
private void UpdatePointCloud(PointData PointData)
{
if (string.IsNullOrEmpty(PointData.mapName))
{
PointCloudParticleSystem.Clear();
return;
}
if (!PointCloudParticleSystem)
{
return;
}
var particles = PointData.PointCloud.Select(p =>
{
var particle = new ParticleSystem.Particle();
particle.position = p;
particle.startLifetime = pointCloudParticleParameter.StartLifetime;
particle.remainingLifetime = pointCloudParticleParameter.RemainingLifetime;
particle.startSize = pointCloudParticleParameter.StartSize;
particle.startColor = pointCloudParticleParameter.StartColor;
return particle;
}).ToArray();
PointCloudParticleSystem.SetParticles(particles, particles.Length);
}
如下如所示: 这是我扫描的一个门的场景。
这样就拿到点云了。
操作 mapData 对象。这个 mapData ,就是当前操作的 地图数据 ,就是 DebugObj(Index) 方法的第一行赋值的。
第三部分:编辑!Unity端
在Play 模式下。
往场景中添加自己想要放置的预设体。注意更改对象的名字, 去掉 (Clone)。
编辑完成后,点击SceneMaster 游戏对象。将刚才操作的对象拖拽进Props数组。
此时点击 保存SaveMeta 按钮。保存当前操作的 mapMeta。
这样就完成了场景编辑,这时退出Play模式就行了。
然后将所有地图中引用的 游戏预设体 拖拽进 场景中的 PropCollection 游戏对象。并作适当的适配。
这时,不要忘记把windows路径下的SparseSpatialMap文件夹下所有的文件和安卓路径下SparseSpatialMap文件夹内容替换更新一下。
之后就是客户端扫描了。
我把meta信息放在本地了,大家可以弄个服务器放到服务器上,这样就不用来回拷贝了,操作同一个文件方便一些。
在手机端编辑场景的拖拽逻辑参考EasyAR自带的逻辑 Assets/Samples/Scenes/WorldSensing/SpatialMap_SparseSpatialMap.unity 里 修改而来的。