【Unity-学习-015】EasyAR4.0稀疏空间地图 编辑场景功能

本帖主要描写编辑场景的功能实现,以及一些需要注意的问题。跟上层贴有所关联,想要更多了解请移步链接

上一篇写的 编辑场景 帖子太细了,觉得没有必要。之后主要描述代码。

其实编辑功能主要是将 从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/
windowsC:\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 里 修改而来的。

 

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ThursdayGame

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值