UE4从零开始制作数字孪生道路监测平台

UE4集成Cesium for Unreal和WebSocket,后端使用NodeJs搭建服务器进行数据模拟和真实数据实时转发。

UE4从零开始制作数字孪生道路监测平台

1:新建UE4项目并集成Cesium for Unreal

Cesium for UE4插件解锁了虚幻引擎中的3D地理空间生态系统。通过将高精度全尺寸WGS84 globe、开放API和空间索引开放标准(如3D图块)以及基于云的真实世界3D内容与虚幻引擎的强大功能相结合,我们将能够使用游戏引擎创建利用真实世界内容的地理空间应用程序的新时代。

1.1:获取Cesium for UE4 插件

  1. 先安装UE4引擎,需要4.26及以上的,我之前用的是4.25.4,现在需要换成4.26.2。
  2. 插件地址:Cesium for Unreal in Code Plugins - UE Marketplace (unrealengine.com)
  3. 登录epic账号,点击页面中的Free之后再点击在启动浏览器打开,就会自动启动Epic Games Launcher。
  4. 下载这个插件即可

1.2:使用&加载插件

创建好一个游戏项目

加载该插件

进入到程序,转到 编辑-> 插件并在插件窗口右上角的插索栏中搜索"Cesium",确保选中插件的启用复选框

UE4从零开始制作数字孪生道路监测平台

编辑→项目设置→自动曝光 来修改曝光

UE4从零开始制作数字孪生道路监测平台

然后把初始场景中的所有对象都删除

UE4从零开始制作数字孪生道路监测平台

Ctrl+S保存关卡,取一个名字addam-cesium

打开菜单编辑->项目设置,再选择项目->地图和模式。将编辑器开始地图游戏默认地图设置为刚创建的关卡addam-cesium。这样可以确保在重新启动虚幻编辑器时自动重新打开您的关卡。

UE4从零开始制作数字孪生道路监测平台
UE4从零开始制作数字孪生道路监测平台

1.3:添加SunSky照明和DynamicPawn

在内容管理器右下角→视图选项中打钩显示引擎内容和显示插件内容

UE4从零开始制作数字孪生道路监测平台

在内容中搜索CesiumForUnreal以打开CesiumForUnreal内容

UE4从零开始制作数字孪生道路监测平台

CesiumSunSky蓝图给室外场景增加了炫酷的天阳光照明,它扩展了内置的SunSky蓝图,让它跟真实地球上的太阳光一模一样。

添加Cesium的太阳光,一定要用Cesium的不能用自己的太阳光。

UE4从零开始制作数字孪生道路监测平台

同样,Cesium的DynamicPawn扩展了内置的DynamicPawn,使其能在地球上任意移动,并允许使用鼠标滚轮控制移动速度,特别是距离地面很远时非常有用(用键盘WSAD前后左右移动,鼠标控制方向,滚轮控制移动速度)。

当视角在Cesium地球上方的不同位置之间飞行时,照相机应遵循与地球表面平行的弯曲路径,而不是线性的点对点飞行轨迹。Cesium的FloatingPawn就是做这个用的,它确保你从地球的一边飞到地球的另外一边时,向上的方向始终从地球的球心指向天空(直观的理解就是:因为地球是园的,不是平的,所以虚幻引擎默认的相机移动方式就不行了)。

在此之后需要设置玩家为玩家0

1.4:连接到Cesium ion并创建一个地球

点击Connect按钮,连接到Cesium ion。这时浏览器会自动打开,要求您允许Cesium for Unreal使用Cesium ion中当前登录的帐户访问您的资产:

通过单击工具栏中的Cesium按钮来打开Cesium面板。在Quick Add中,单击Cesium World Terrain + Bing Maps Aerial imagery右侧的加号(可以随意添加其他Cesium World Terrain + imagery组合之一)。 、

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0h1pw3CQ-1675041397096)(UE4从零开始制UE4从零开始制作数字孪生道路监测平台
作数字孪生道路监测平台.assets\1671092251066.png)]

此步骤将在世界大纲视图中生成新的Cesium World TerrainCesiumGeoreference蓝图对象。

如果场景太暗,请调整CesiumSunSky对象上的Solar Time属性,比如设置一个美丽的日落场景。您可能还想更改时区Time Zone

1.5:加载本地3Dtiles

(1)加载本地3Dtiles

再已经构建好的地图上添加Blank 3D Tiles Tileset,这步操作将往场景中添加一个新的、空白的Cesium3DTileset Actor。 之后世界大纲视图就会出现这个,改个名字,我的是改成AddamTileset

双击再细节页面就会出现详细信息

Source改成From Url

下面Url改成本地文件地址,我的是

D:\GameProject\MyModel\nantaizihu\tileset.json就得改成file:///D:/GameProject/MyModel/nantaizihu/tileset.json

file:///D:/GameProject/MyModel/nantaizihu/tileset.json

http://139.9.62.222/static/nantaizihu/tileset.json

在细节面板中调整LocalTileset的位置Location,将其放置在需要的地方。例如,要将3D Tileset贴在地形上,请将Location的Z坐标更改为7400。

接下来在世界大纲视图中找到CesiumGeoreference,双击细节面板上点击Place Georeference Origin Here,这个会校准我们的世界起始位置的经纬度,并且把相机DynamicPawn的位置重置为(0,0,0)

UE4从零开始制作数字孪生道路监测平台

目前Cesium for Unreal不支持加载本地quantized-mesh地形文件。但是,可以从任何服务器(包括本机localhost)加载地形资产。要完全离线加载地形资产,请考虑设置本机localhost地形服务。可以使用与上述相同的步骤,用http://localhost:portNumber/terrainAsset设置Url属性。

PS:也可以考虑使用Cesium的Ion功能,将3D地图上传到Cesium官网

UE4从零开始制作数字孪生道路监测平台

My Assets | Cesium ion

然后UE4中

UE4从零开始制作数字孪生道路监测平台

(2)使用制图多边形隐藏切片集的某些部分

现在虽然3Dtiles加上了,但是会有问题,类似于这种地方其实是Cesium初始地球地形穿模导致,这个时候我们可以使用制图多边形隐藏切片集的某些部分。

首先添加和塑造多边形

参与者添加到关卡。您可以通过在“放置Actor”面板中搜索并将其拖动到关卡中来查找该Actor,或者只需单击一下即可从“Cesium快速添加”面板中添加它。

该执行组件看起来像一个方形样条,在闭合环中具有四个控制点。目前,样条曲线只能是线性的,并且不允许使用曲线。这样可以更快地进行栅格化。

可以选择一个点移动该点位置,要移动其中一个点,请在视口中单击该点以选择该点。使用变换小控件更改其位置。

若要添加新点,请按住键盘上的 alt 键,然后在按住 alt 键的同时移动一个点。将创建一个新点。也可以右键单击样条线点,然后选择“重复点”。 Alt+鼠标左键选择一个点,之后复制一个点,然后四边形就变成五边形了。

让这个和模型边缘贴合(因为模型部分就不需要加载Cesium地球底图了),在和模型契合的时候可以把这个线换个颜色。

UE4从零开始制作数字孪生道路监测平台

要删除某个点,请右键单击该点,然后选择“删除点”。

创建多边形的更多提示和技巧:

之后将多边形连接到切片集

选择自己想要去除该部分的蓝图,我选择的是Cesium World Terrain(Cesium的地球地图)

点击添加组件搜索Cesium Polygon Raster Overlay

给这个Cesium Polygon Raster Overlay 添加一个Polygon并设置为刚才的Blank 3D Tiles Tileset

最终效果就完成啦。

1.6:去除Cesium的logo

这个Cesium的logo一运行就会有

我们找到CesiumCreditSystemBP,编辑蓝图

在细节面板搜索Credits Widget Class,改成none。改了之后由于没有这个了Cesium会出点问题,但是不影响整体运行。

UE4从零开始制作数字孪生道路监测平台

1.7:最终效果呈现

UE4从零开始制作数字孪生道路监测平台

2:关卡蓝图添加WS通信并设计车辆蓝图类

2.1:集成HTTP & WebSocket Helper

也可以使用源码直接编译的方式

链接:https://pan.baidu.com/s/1pxO0farSSNki6O-o5J5jEg
提取码:ougf

可以去我分享的百度网盘下载库文件,然后引入把文件夹解压放到UE_4.26\Engine\Plugins下。然后重启UE搜索HTTP就有这个了。

2.1:介绍一下HTTP & WebSocket Helper

1)WebSocketOptions

创建一个蓝图类Actor

在时间图表中,创建一个创建WebSocketOptions,这个

Protocol可以选择是ws还是wss

Url就输入ws的地址和端口,例如我的测试端口就是127.0.0.1:8800

该节点输出就是WebSocketOptions

2)AsyncWebSocket

后面再连接一个AsyncWebSocket,异步节点,用于使用指定的协议和标头建立 websocket 通信 .

输入是:WebSocketOptions ,输出就是WebSocketResult

比如我们现在的需求是,创建连接页面上输出Hello,之后有消息过来就把这个消息给输出到页面,如果连接关闭输出一个“连接失败”。

这个时候需要另一个节点Break WebSocketResult

3)HTTP & WebSocket Helper节点汇总
Node 输入 输出 Note
CreateWebSocket void WebSocketHandler 将处理程序保存到变量中以便于访问
BindEventToOnConnected Callback Event void 连接到对等方时调用
BindEventToOnConnectionError Callback Event ErrorReason(String) 连接发生错误时调用
BindEventToOnClosed Callback Event Code(Int), Reason(String), ClosedByPeer(Bool) 当通信因错误或对等方关闭时调用
BindEventToOnTextMessage Callback Event Message(String) 接收对等方短信时调用
BindEventToOnByteMessage Callback Event Message(Array(Byte)) 在接收对等方的原始消息时调用,无论是文本消息还是二进制消息
BindEventToOnConnectionRetry Callback Event RetryCount(Int) 在出错后重试打开通信时调用
BindEventToOnMessageSent Callback Event Message(String) 发送短信时调用
Open WebSocketOptions Result(Bool) 尝试打开连接,如果 websockethandler 发送了 open 命令,则返回 true,要知道连接是否成功建立,请将事件绑定到 OnConnected
Close Code(Int), Reason(String) Result(Bool) 尝试关闭连接,如果 websockethandler 发送了 close 命令,则返回 true,以了解连接是否已终止,将事件绑定到 OnClose
IsConnected void Result(Bool) 告知 Websockethandler 是否与对等方进行了打开的通信
SendBytes Data(Array(Byte)), IsBinary(Bool) Result(Bool) 尝试通过通信发送字节
SendText Data(String) Result(Bool) 尝试通过通信发送字符串

2.2:编写蓝图函数实现Json字符串转Json

1)创建一个C++类AddamCar

新建一个AddamCar的C++Actor类,记得设立为公有函数

在AddamCar.h文件中添加几个属性和一个方法

// EditAnywhere
UPROPERTY(Category = "AddamCar", BlueprintReadOnly)
float lon = 114.0;
UPROPERTY(Category = "AddamCar", BlueprintReadOnly)
float lat = 30.0;
UPROPERTY(Category = "AddamCar", BlueprintReadOnly)
float azimuth = 100;
UFUNCTION(Category = "AddamCar", BlueprintCallable)
void String_json(FString addad);

在AddamCar.cpp中实现这个方法

先include几个必要的包,这里使用到的包是rapidjson

#include <string>
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"

然后实现上面定义的String_json函数

void AAddamCar::String_json(FString addad)
{
	std::string lll(TCHAR_TO_UTF8(*addad));
	const char* json = lll.data();
	rapidjson::Document d;
	d.Parse(json);
	// 2. 利用 DOM 作出修改。
	// Value& s = d["longitude"];
	// s.SetInt(s.GetInt() + 1);
	lon = d["data"]["vehicleInfo"]["longitude"].GetDouble();
	lat = d["data"]["vehicleInfo"]["latitude"].GetDouble();
	azimuth = d["data"]["vehicleInfo"]["azimuth"].GetDouble();
}
2)C++类生成汽车的FBX模型

接下来右键改C++文件,创建基于AddamCar的派生类

之后找到一份小车的模型拖入任务管理器新建好的文件夹内

双击刚才生成的蓝图打开完整的蓝图编辑器

把转好的小车模型拖入蓝图的视口中

2.3:关卡蓝图接收数据

车辆信息以cjson格式上报的websocket服务器,然后服务器转发给UE4客户端,整体格式如下:

{
    type:数据类型标识
    timestamp:时间
    id:车辆唯一id
    data:{	
        vehicleInfo:车身基本信息
        roadPoints:路径点集合
    }
}
1)编写setPosition函数修改车辆位置

现在关卡蓝图中创建一个函数名叫setPosition,用于移动AddamCar模型

UE4从零开始制作数字孪生道路监测平台

我们一起来解读一下这个蓝图函数

先把AddamCar刚才设置的全局变量Lat和Lon以及高度一起创建向量,并使用CesiumGeoreferenceInaccurate Transform Longitude Latitude Height to Unreal方法把这个Vector转化为Unreal坐标,并设置位置。

后面的设置旋转就直接设置z轴旋转就好,很简单就不说了。

2)关卡蓝图创建WebSocket连接并使用setPosition

还是先看蓝图

UE4从零开始制作数字孪生道路监测平台

事件开始运行的时候创建连接,并在连接成功时打印WebSocket连接成功,在连接出错活失败时打印WebSocket连接失败

UE4从零开始制作数字孪生道路监测平台

当有数据过来时可以获取数据并作为参数调用AddamCar里面写的String Json函数,把Json解析把经纬度赋给全局变量,之后判断消息类型,固定消息调用调用setPosition函数。

UE4从零开始制作数字孪生道路监测平台

-------第一阶段小总结----------------------------------------------------------------------

这一阶段实现了车辆在路上跑,就跟随实时数据进行实时仿真,但是会有很大的问题,UE4的浮点数实际能精确到的只是小数点后6位,只有单精度浮点数,经纬度的小数点后6位会带来最大10cm的误差,这导致显示效果在屏幕上会看着很卡。Inaccurate Transform Longitude Latitude Height to Unreal前面的Inccurate是不精确的,所以之后只能编写C++函数来进行数据的转化,就直接传入转好的坐标,Unreal坐标1是1cm,所以小数点后六位精度肯定是绰绰有余。

3:解决数据精度不够的问题

3.1:修改Cesium源码添加函数

不想修改的可以直接下载我修改好的库文件源码

链接:https://pan.baidu.com/s/1-1VcOakrbiwXtn0gsrJAig
提取码:o6cf

UE4 的蓝图的精度现在只局限在float(16位),如果要来表示经纬度,它的精度是远远不够的。但是double(32位)来表示我们的经纬度却绰绰有余,我们如何能够保证在使用蓝图的时候,也能够用足够的精度来表示经纬度呢?

我们的都知道C++代码其实是可以用double双精度,于是就可以想到用C++来写两个我们蓝图可以调用的函数,以实现UE4与经纬度之间的转化,怎样实现呢?由于蓝图中不能出现double,那我们可以转变下思路,在蓝图中将经纬度用字符串的形式来表示。

我们需要写两个函数,一个将虚幻的坐标转化为经纬度的字符串,一个将经纬度的字符串转化为虚幻的坐标,这些函数都是在C++中实现的(因为只有在C++代码中才可以使用double),这两个函数的代码如下:

// 将ue坐标转化为经纬度字符串
FString ACesiumGeoreference::AccurateTransformUnrealToLongitudeLatitudeHeightString(
      const FVector& ue) const{
      glm::dvec3 llh =
      this->_geoTransforms.TransformUnrealToLongitudeLatitudeHeight(
          glm::dvec3(CesiumActors::getWorldOrigin4D(this)),
          VecMath::createVector3D(ue));
        return FString::SanitizeFloat((double)llh.x)+","+ FString::SanitizeFloat((double)llh.y)+","+ FString::SanitizeFloat((double)llh.z);
  }
// 将经纬度字符串转化为ue坐标
FVector ACesiumGeoreference::AccurateTransformLongitudeLatitudeHeightFStringToUnreal(const FString& sLongitudeLatitudeHeight) const{
    TArray<FString> stringArray;
    sLongitudeLatitudeHeight.ParseIntoArray(stringArray, TEXT(","), false);
    glm::dvec3 TargetLongitudeLatitudeHeight = glm::dvec3(FCString::Atod(*stringArray[0]), FCString::Atod(*stringArray[1]), FCString::Atod(*stringArray[2]));
    glm::dvec3 t = TransformLongitudeLatitudeHeightToUnreal(TargetLongitudeLatitudeHeight);
    return FVector(t.x,t.y,t.z);
}

其中我们需要CesiumGeoreference中的两个函数,所以我直接将两个转化函数写到了CesiumGeoreference的源码里边的,大家如果不想该Cesium for Unreal的源码的话,可以写一个蓝图库函数,将reference作为参数传入,然后再调用了TransformLongitudeLatitudeHeightToUnreal之类的精确转化函数。Cesium插件也给我们提供了一个叫GetDefaultReference(),可以直接获取当前关卡的CesiumGeoreference。

由于我们需要直接在蓝图中对这两个函数进行调用,我们在.h文件中声明的时候,需要加上BlueprintCallable关键字

UFUNCTION(BlueprintCallable, Category = "Cesium")
FString AccurateTransformUnrealToLongitudeLatitudeHeightString(
      const FVector& Unreal) const;
UFUNCTION(BlueprintCallable, Category = "Cesium")
FVector AccurateTransformLongitudeLatitudeHeightFStringToUnreal(const FString& sLongitudeLatitudeHeight) const;

3.2:在UE4中编译Cesium for Unreal插件源码

将Cesium for Unreal的插件从引擎的目录放到项目的目录里边。项目在编译的时候不会去编译引擎目录下得插件代码。我们在Epic商城中下载的插件的默认的位置是在这个目录下边:

UE4从零开始制作数字孪生道路监测平台

2.我们需要将这个插件目录copy到项目的plugins目录下,初始时我们的项目目录下是没有这个plugins目录的,需要我们自己去创建一个。copy好了之后,进入插件目录,将binaries目录删掉。因为我们需要让虚幻自己检测到这个插件的Binaries被删掉了,让虚幻帮我们重建。

3.由于我们需要让项目知道我们用到了Cesium插件,同时我们也希望在VS或者Rider中对Cesium插件的源码进行编辑。一般我们用vs加载项目,我们可以看到解决方案里边是没有plugins目录,所以需要我们手动到.uproject文件中添加引用到的插件。这样我们就可以在IDE中,直接编辑插件的源码,而且还有代码补全。如果你的.uproject文件中已经有了Cesium的声明,那么请忽略这一步。

"Plugins": [
		{
			"Name": "CesiumForUnreal",
			"Enabled": true,
			"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/87b0d05800a545d49bf858ef3458c4f7",
			"SupportedTargetPlatforms": [
				"Win64",
				"Mac",
				"Linux",
				"Android",
				"IOS"
			]
		},
		{
			"Name": "HttpHelper",
			"Enabled": true,
			"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/d6e73d57925e4acf89649cea5b686e86"
		}
	]

4.接下来咱们就可以修改Cesium的源码啦。

5.在我们修改好源代码之后,接下来的步骤就非常非常简单了。直接进入项目目录中,双击.uproject文件,由于我们将插件的Binaries文件夹删除,虚幻会提醒我们进行rebuild,我们只用点击确认,虚幻就能自己帮我们构建。

这样我们的插件源代码就重建好了。

3.3:修改原本的蓝图

新建一个C++类,Character的C++类(后面解释原因),其取名为MyCharacterCar,在MyCharacterCar.h中加入如下代码

// EditAnywhere
UPROPERTY(Category = "AddamCar", BlueprintReadOnly)
    float lon = 114.0;
UPROPERTY(Category = "AddamCar", BlueprintReadOnly)
    float lat = 30.0;
UPROPERTY(Category = "AddamCar", BlueprintReadOnly)
    FString LonLatStr;
UPROPERTY(Category = "AddamCar", BlueprintReadOnly)
    float azimuth = 100;
UFUNCTION(Category = "AddamCar", BlueprintCallable)
    void String_json(FString addad);

在MyCharacterCar.cpp中实现String_json方法,高度暂且设置为18

void AMyCharacterCar::String_json(FString addad)
{
	std::string lll(TCHAR_TO_UTF8(*addad));
	const char* json = lll.data();
	rapidjson::Document d;
	d.Parse(json);
	// 2. 利用 DOM 作出修改。
	// Value& s = d["longitude"];
	// s.SetInt(s.GetInt() + 1);
	lon = d["data"]["vehicleInfo"]["longitude"].GetDouble();
	lat = d["data"]["vehicleInfo"]["latitude"].GetDouble();
	azimuth = d["data"]["vehicleInfo"]["azimuth"].GetDouble();
	double lonssssss = d["data"]["vehicleInfo"]["longitude"].GetDouble() / 10000000;
	double latssssss = d["data"]["vehicleInfo"]["latitude"].GetDouble() / 10000000;
	std::stringstream ss;
	ss << std::setprecision(15) << lonssssss;
	std::stringstream ss2;
	ss2 << std::setprecision(15) << latssssss;
	//22.95-147506.0
	LonLatStr = (ss.str() + "," + ss2.str() + "," + "18").data();
}

之后还是生成一个基于该C++类的蓝图类

之后去关卡蓝图中的事件图表编辑一下全图事件

修改一下setPosition方法

里面的AccurateTransformLongitudeLatitudeHeightFStringToUnreal就是我们之前添加到Cesium for Unreal源码中的方法,输入是FString经度,纬度,高度,输出还是Unreal坐标向量。

4:车辆添加相机跟随

4.1:车辆添加相机跟随

在模型上添加一个弹簧臂,再添加一个摄像机,为了避免物体下坠带来的抖动设置车辆模型重力为0.

之后为防止障碍物遮挡修改设置Actor位置的地下两个参数,使得每次直接传送过去而不是开过去。

4.2:设置Tab键进行视角切换

细节查看(3条消息) UE4入门笔记一(begin in 20221021)_Addam Holmes的博客-CSDN博客

我之前写的第5.7节,其实类似的

添加一个全局变量是否能够切换完成避免有人手剑狂点

使用MultGate做分支,具体怎么使用见之前那篇帖子的5.7节

切换全局视角还是车辆跟随,这个其实类似

就是上面的四个节点可以重复利用就创建一个宏把他们包裹起来

UE4从零开始制作数字孪生道路监测平台

视角切换的效果还是很不错的

4.3:阶段性总结

这一阶段实现了车辆在路上跑并且实现视角跟随,并且编写了函数进行精确的位姿计算,最终效果还是不错的,但是有很大的弊端,首先就是车辆只有一辆无法进行多车的信息接收,然后就是感知数据无法进行展示,最后就是车辆未来轨迹规划也没显示出来。接下来一步步都会添加上这些功能。

5:添加感知车辆(一种类型的车辆)

perceptualInfo中有一个包含多个元素的数组,每个元素的定义如下

id:感知车辆编号

type:车辆类型

longitude latitude:经纬度

azimuth:感知车辆转向角

感知数据整体定义如下:

{
    "type": "perceptualInfo",
    "id": "1",
    "timestamp": 1663752684007,
    "data": [
        {
            "id": 0,
            "type": "car",
            "longitude": 1141788026,
            "latitude": 304915364,
            "azimuth": -7487
        },
        {
            "id": 100,
            "type": "car",
            "longitude": 1141774257,
            "latitude": 304918251,
            "azimuth": 28500
        },
}

5.1:Spawn Actor from Class

使用Spawn Actor from Class

Spawn Actor from Class 节点带有一个 Actor 类(通过类输入指定并尝试在世界中生成一个属于该类的实例。生成变换(Spawn Transform)输入用于定义 Actor 在世界中的生成位置(及其初始方向)。如果这个位置被碰撞体阻挡,Actor 将无法生成,除非将“即使碰撞也生成”(Spawn Even if Colliding)输入设置为 True。

Spawn Actor 节点上的每个针都如以下介绍:

数字 说明
Execution Input (1) 这是一个执行输入,用于触发节点来生成 Actor。
Class (2) 这是您在世界中生成的实例所属的 Actor 类(必需)。
Spawn Transform (3) 这种变换用于对世界中的 Actor 进行定位和定向。
Collision Handling Override(4) 说明如何生成点出的碰撞
Execution Output (5) 这是一个执行输出,可以触发在“生成 Actor”之后出现的脚本。
Return Value (6) 可以输出世界中生成的新 Actor 实例。

5.2:前期准备

新建C++类(Actor类)MyPerceiveCar

里面添加一个通信变量id

UPROPERTY(Category = "PerceiveCar", BlueprintReadWrite)
	int64 id;

这个是感知车辆的唯一标识符

之后基于MyPerceiveCar建立蓝图类MyMyPerceiveCar并添加上车辆模型

UE4从零开始制作数字孪生道路监测平台

5.3:模拟的车辆感知数据格式

数据类型如下:

{
  "type": "perceptualInfo",
  "id": "1",
  "timestamp": 1663810076677,
  "data": [
    {
      "id": 600,
      "type": "car",
      "longitude": 1141789547,
      "latitude": 304969280,
      "azimuth": 10503
    }
  ]
}

关于rapidjson解析数组可见

(41条消息) rapidjson解析数组过程_shyzzjf的博客-CSDN博客_rapidjson 解析数组

(41条消息) rapidjson官方教程_H-KING的博客-CSDN博客_rapidjson教程

5.4:解析数据并分类

感知数据主要分为三大类数据新增修改位置删除

由于现在暂且只是一种车辆所以就先就这样的逻辑

创建Actor的子类PerceiveControler

添加变量和声明函数

AddId,AddLonLat,AddType,AddAzimuth为新增的数据,UE4中有TMap,那为什么不TArray写一起呢,好的,原因就是这样写编译不通过。

UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<int64> AddId;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> AddLonLat;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> AddType;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<float> AddAzimuth;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<int64> MoveId;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> MoveLonLat;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> MoveType;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<float> MoveAzimuth;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<int64> DeleteId;
std::vector<int64> PerceiveIdOld;
UFUNCTION(Category = "perceiveFunction", BlueprintCallable)
    void distinguishPerceive(FString mssage);

distinguishPerceive函数是解析感知json数据的,把数据序列化赋给上面声明的几个变量,然后在蓝图中去使用变量进行增加删除修改模型。

PerceiveIdOld是列表,用来记录上的车辆的编号,逻辑就是

  1. 如果这一帧存在某个编号的数据并且上一帧数据不存在,那么就把数据序列化给增加Add的数据。
  2. 如果这一帧不存在某个编号的数据并且上一帧数据存在,那么就把数据序列化给删除Delete的数据。
  3. 如果这一帧存在某个编号的数据并且上一帧数据存在,那么就把数据序列化给移动Move的数据。

逻辑是非常的简单非常的银杏化。

distinguishPerceive见代码

void AMyCharacterCar::distinguishPerceive(FString mssage)
{
	// 清空数组
	AddId.Empty();
	AddLonLat.Empty();
	AddType.Empty();
	AddAzimuth.Empty();
	MoveId.Empty();
	MoveLonLat.Empty();
	MoveType.Empty();
	MoveAzimuth.Empty();
	DeleteId.Empty();
	DeleteType.Empty();
	std::string lll(TCHAR_TO_UTF8(*mssage));
	const char* json = lll.data();
	rapidjson::Document d;
	d.Parse(json);
	rapidjson::Value& perceiveData = d["data"];
	std::string type;
	int64 PerceiveId, PerceiveAzimuth;
	if (perceiveData.IsArray())
	{
		std::vector<int64> LinShiId;
		std::vector<std::string> LinShiType;
		for (size_t i = 0; i < perceiveData.Size(); ++i)
		{
			rapidjson::Value& v = perceiveData[i];
			assert(v.IsObject());
			type = v["type"].GetString();
			PerceiveId = v["id"].GetInt();
			PerceiveAzimuth = v["azimuth"].GetFloat();
			double lonssssss = v["longitude"].GetDouble() / 10000000;
			double latssssss = v["latitude"].GetDouble() / 10000000;
			std::stringstream ss1;
			ss1 << std::setprecision(15) << lonssssss;
			std::stringstream ss2;
			ss2 << std::setprecision(15) << latssssss;
			std::string PerceiveLonLatStr = (ss1.str() + "," + ss2.str() + "," + "18").data();
			LinShiId.push_back(PerceiveId);
			LinShiType.push_back(type);
			if (std::find(PerceiveIdOld.begin(), PerceiveIdOld.end(), PerceiveId) != PerceiveIdOld.end()) {
				for (int iii = 0; iii < PerceiveIdOld.size(); ++iii) {
					if (PerceiveIdOld[iii] == PerceiveId)
					{
						std::vector<int64>::iterator  it1 = PerceiveIdOld.begin() + iii;
						std::vector<std::string>::iterator  it2 = PerceiveTypeOld.begin() + iii;
						PerceiveIdOld.erase(it1);
						PerceiveTypeOld.erase(it2);
						break;
					}
				}
				MoveId.Add(PerceiveId);
				MoveType.Add(UTF8_TO_TCHAR(type.c_str()));
				MoveLonLat.Add(UTF8_TO_TCHAR(PerceiveLonLatStr.c_str()));
				MoveAzimuth.Add(PerceiveAzimuth);
			}
			else {
				AddId.Add(PerceiveId);
				AddType.Add(UTF8_TO_TCHAR(type.c_str()));
				AddLonLat.Add(UTF8_TO_TCHAR(PerceiveLonLatStr.c_str()));
				AddAzimuth.Add(PerceiveAzimuth);
			}
		}
		for (int i = 0; i < PerceiveIdOld.size(); ++i)
		{
			DeleteId.Add(PerceiveIdOld[i]);
			DeleteType.Add(UTF8_TO_TCHAR(PerceiveTypeOld[i].c_str()));
		}
		PerceiveIdOld = LinShiId;
		PerceiveTypeOld = LinShiType;
	}
}

需要include的头文件

#include <sstream>
#include <iomanip>
#include <string>
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"

5.5:新增车辆的函数

新建函数

  1. setCarPosition:就是之前写的移动主车辆的函数
  2. PerceiveControler:拆解数据,然后循环数组调用增加,删除,移动感知车辆的函数
  3. AddPerceive:新增感知车辆,由于无论何时AddId,AddLonLat,AddType,AddAzimuth四个TArray通信变量的元素个数都是相等,且对应下标的数据都是对应的,所以AddPerceive函数传入的参数就是数组AddId的index下标。
  4. MovePerceive:移动感知车辆,传入的参数就是数组MoveId的index下标。
  5. DeletePerceive:删除感知车辆,传入的参数就是数组DeleteId的index下标。

5.6:移动和删除车辆

查找Actor的三种方法

  1. Get All Actors Of Class :通过给定的对象模板将场景中与之匹配的对象进行查找,并返回查找到的所有对象。
  2. Get All Actors with Interface:通过给定的接口将场景中与之匹配的对象进行查找,并返回查找到的所有对象。
  3. Get All Actors with Tag:通过给定的标签将场景中与之匹配的对象进行查找,并返回查找到的所有对象。

修改完毕之后的PerceiveControler蓝图如下(就add的循环完毕之后就调用move的循环,最后调用delete的循环)

-------第二阶段小总结----------------------------------------------------------------------

第二阶段主要就是车辆感知数据的展示比较麻烦,难点主要是解析json,对模型的增加删除修改,蓝图和C++代码的Array通信,第二阶段完成之后第三阶段主要的任务:

感知车辆的类型可以多样化,先由原本的只有小轿车加上货车,面包车,人,这三类车辆(刚好手上有这四类模型)

添加车前轨迹线,之前的车辆移动数据中有一个车前轨迹线数据没有用上,现在也可以添加上。

6:感知车辆多样化

6.1:蓝图类模型准备

第一步导入FBX模型

之前新建的那个C++类可以接着用,其实我们不直接创建Actor的蓝图类就是为了有个id属性,所以基于MyPerceiveCar的C++类我们再创建三个蓝图类,并在视口中给模型的各个组件分别绑定到这个蓝图类上,需要调整大小,调整方向,初始方向必须和之前的感知车辆一样。

UE4从零开始制作数字孪生道路监测平台

UE4从零开始制作数字孪生道路监测平台

6.2:添加type的数据通信

修改PerceiveControler.h

UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<int64> AddId;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> AddLonLat;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> AddType;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<float> AddAzimuth;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<int64> MoveId;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> MoveLonLat;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> MoveType;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<float> MoveAzimuth;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<int64> DeleteId;
UPROPERTY(Category = "PerceiveData", BlueprintReadWrite)
    TArray<FString> DeleteType;
std::vector<int64> PerceiveIdOld;
std::vector<std::string> PerceiveTypeOld;
UFUNCTION(Category = "perceiveFunction", BlueprintCallable)
    void distinguishPerceive(FString mssage);

修改PerceiveControler.cpp

void AMyCharacterCar::distinguishPerceive(FString mssage)
{
	// 清空数组
	AddId.Empty();
	AddLonLat.Empty();
	AddType.Empty();
	AddAzimuth.Empty();
	MoveId.Empty();
	MoveLonLat.Empty();
	MoveType.Empty();
	MoveAzimuth.Empty();
	DeleteId.Empty();
	DeleteType.Empty();
	std::string lll(TCHAR_TO_UTF8(*mssage));
	const char* json = lll.data();
	rapidjson::Document d;
	d.Parse(json);
	rapidjson::Value& perceiveData = d["data"];
	std::string type;
	int64 PerceiveId, PerceiveAzimuth;
	if (perceiveData.IsArray())
	{
		std::vector<int64> LinShiId;
		std::vector<std::string> LinShiType;
		for (size_t i = 0; i < perceiveData.Size(); ++i)
		{
			rapidjson::Value& v = perceiveData[i];
			assert(v.IsObject());
			type = v["type"].GetString();
			PerceiveId = v["id"].GetInt();
			PerceiveAzimuth = v["azimuth"].GetFloat();
			double lonssssss = v["longitude"].GetDouble() / 10000000;
			double latssssss = v["latitude"].GetDouble() / 10000000;
			std::stringstream ss1;
			ss1 << std::setprecision(15) << lonssssss;
			std::stringstream ss2;
			ss2 << std::setprecision(15) << latssssss;
			std::string PerceiveLonLatStr = (ss1.str() + "," + ss2.str() + "," + "18").data();
			LinShiId.push_back(PerceiveId);
			LinShiType.push_back(type);
			if (std::find(PerceiveIdOld.begin(), PerceiveIdOld.end(), PerceiveId) != PerceiveIdOld.end()) {
				for (int iii = 0; iii < PerceiveIdOld.size(); ++iii) {
					if (PerceiveIdOld[iii] == PerceiveId)
					{
						std::vector<int64>::iterator  it1 = PerceiveIdOld.begin() + iii;
						std::vector<std::string>::iterator  it2 = PerceiveTypeOld.begin() + iii;
						PerceiveIdOld.erase(it1);
						PerceiveTypeOld.erase(it2);
						break;
					}
				}
				MoveId.Add(PerceiveId);
				MoveType.Add(UTF8_TO_TCHAR(type.c_str()));
				MoveLonLat.Add(UTF8_TO_TCHAR(PerceiveLonLatStr.c_str()));
				MoveAzimuth.Add(PerceiveAzimuth);
			}
			else {
				AddId.Add(PerceiveId);
				AddType.Add(UTF8_TO_TCHAR(type.c_str()));
				AddLonLat.Add(UTF8_TO_TCHAR(PerceiveLonLatStr.c_str()));
				AddAzimuth.Add(PerceiveAzimuth);
			}
		}
		for (int i = 0; i < PerceiveIdOld.size(); ++i)
		{
			DeleteId.Add(PerceiveIdOld[i]);
			DeleteType.Add(UTF8_TO_TCHAR(PerceiveTypeOld[i].c_str()));
		}
		PerceiveIdOld = LinShiId;
		PerceiveTypeOld = LinShiType;
	}
}

6.3:修改关卡蓝图,不同type修改不同类型

(1)修改PerceiveControler

删除原本的AddPerceive,MovePerceive,DeletePerceive函数

新增全局变量

AddIndex(整形),MoveIndex(整形),DeleteIndex(整形)

没什么用,就是美化蓝图,避免线连得到处都是。

这个是PerceiveControler,用来烤制增删改三个流程,控制的逻辑也非常简单,先增加再更改后删除,比如增加,修改AddIndex,然后调用节点AddControler,Move修改位姿以及Delete同理,先修改对应的Index然后调用节点。这个节点就是一堆蓝图封装成节点看起来舒服点。

(2)AddControler节点

其实AddControler节点也非常简单,就是根据获取到当前数据的type,然后通过不同的type调用不同节点,先判断type是不是car,是car就调用AddCar节点,type不是car就判断是不是person,是person就调用AddPerson节点,以此类推。

UE4从零开始制作数字孪生道路监测平台

和之前的AddControler如出一辙,那么AddPerson,AddTruck,AddVan他们和AddCar有什么区别呢

唯一的区别

UE4从零开始制作数字孪生道路监测平台

AddCar这里的Class是MyMyPerceiveCar,AddPerson这里是MyMyPerceivePerson,AddTruck这里是MyMyPerceiveTruck,AddVan这里是MyMyPerceiveVan

UE4从零开始制作数字孪生道路监测平台

(3)MoveControler

UE4从零开始制作数字孪生道路监测平台

逻辑和AddControler不能说很像,只能说是一模一样,我就不多赘述了。

MoveCar

UE4从零开始制作数字孪生道路监测平台

MovePerson就是把获取所有类的actor里面的ActorClass改为MyMyPerceivePerson

(4)DeleteControler

UE4从零开始制作数字孪生道路监测平台

deleteCar

UE4从零开始制作数字孪生道路监测平台

7:添加车前的轨迹线

之前车辆数据中的轨迹线数据我一直没使用上,这个是不合理的,数据存在就一定要用上。

新增一个全局变量和一个修改数据的函数

// 绘制车前轨迹线
UPROPERTY(Category = "AddamCar", BlueprintReadWrite)
    TArray<FString> roadPointsLonLat;
UFUNCTION(Category = "AddamCar", BlueprintCallable)
    void DrawRoadPoints(TArray< FVector> mVertexes);

roadPointsLonLat里面存的是可以被CesiumForUnreal使用的字符串数组,一个字符串代表一个点,点连线就是轨迹线

DrawRoadPoints是一个绘制线段的函数,传参是mVertexes是解析之后的点,经纬度转UE4坐标还是得用蓝图完成的,解析完毕的坐标数组作为参数传给DrawRoadPoints函数进行线段的绘制

第一步:修改原本的车辆数据解析函数

原本车辆有一个数据解析函数String_json

void AMyCharacterCar::String_json(FString addad)
{
	std::string lll(TCHAR_TO_UTF8(*addad));
	const char* json = lll.data();
	rapidjson::Document d;
	d.Parse(json);
	// 2. 利用 DOM 作出修改。
	// Value& s = d["longitude"];
	// s.SetInt(s.GetInt() + 1);
	azimuth = d["data"]["vehicleInfo"]["azimuth"].GetDouble();
	double lonssssss = d["data"]["vehicleInfo"]["longitude"].GetDouble() / 10000000;
	double latssssss = d["data"]["vehicleInfo"]["latitude"].GetDouble() / 10000000;
	std::stringstream ss1;
	ss1 << std::setprecision(15) << lonssssss;
	std::stringstream ss2;
	ss2 << std::setprecision(15) << latssssss;
	LonLatStr = (ss1.str() + "," + ss2.str() + "," + "18").data();
	// 获取轨迹线数据
	roadPointsLonLat.Empty();
	rapidjson::Value& roadPointsData = d["data"]["roadPoints"];
	if (roadPointsData.IsArray())
	{
		for (size_t i = 0; i < roadPointsData.Size(); i = i + 2)
		{
			double roadlon = roadPointsData[i].GetDouble() / 10000000;
			double roadlat = roadPointsData[i+1].GetDouble() / 10000000;
			std::stringstream roadlonString;
			roadlonString << std::setprecision(15) << roadlon;
			std::stringstream roadlatString;
			roadlatString << std::setprecision(15) << roadlat;
			FString roadPointsLonLatStr = (roadlonString.str() + "," + roadlatString.str() + "," + "18").data();
			roadPointsLonLat.Add(roadPointsLonLatStr);
		}
	}
}

主要的改动就是在末尾添加上对轨迹线数据的解析

// 获取轨迹线数据
roadPointsLonLat.Empty();
rapidjson::Value& roadPointsData = d["data"]["roadPoints"];
if (roadPointsData.IsArray())
{
    for (size_t i = 0; i < roadPointsData.Size(); i = i + 2)
    {
        double roadlon = roadPointsData[i].GetDouble() / 10000000;
        double roadlat = roadPointsData[i+1].GetDouble() / 10000000;
        std::stringstream roadlonString;
        roadlonString << std::setprecision(15) << roadlon;
        std::stringstream roadlatString;
        roadlatString << std::setprecision(15) << roadlat;
        FString roadPointsLonLatStr = (roadlonString.str() + "," + roadlatString.str() + "," + "18").data();
        roadPointsLonLat.Add(roadPointsLonLatStr);
    }
}

先清空roadPointsLonLat数组,然后再遍历数据,数据是[lon,lat,lon,lat…]的形式,遍历一遍全部转化为lon,lat,高度<这里预设18>的FString类型的字符串并存到roadPointsLonLat数组中

UE4从零开始制作数字孪生道路监测平台

在关卡蓝图中StringJson就会吧这个数据编译到

之后新建一个setRoadPoints的函数

这个函数主要就是把字符串的经纬度变为UE4坐标的向量

所以毫无疑问,输入就是刚才的roadPointsLonLat数组

输出给个名字RoadArray

UE4从零开始制作数字孪生道路监测平台

在该函数内部创建一个全局变量Result,类型是一个向量数组,逻辑也很简单,遍历roadPointsLonLat数组,一个元素化成一个向量存到Result中,然后用返回节点吧Result作为参数返回出去。

UE4从零开始制作数字孪生道路监测平台

之后在车的C++中新建一个函数,也就是刚才的DrawRoadPoints

void AMyCharacterCar::DrawRoadPoints(TArray<FVector> mVertexes)
{
	TArray<FBatchedLine> lines;
	ULineBatchComponent* const LineBatchComponent = GetWorld()->PersistentLineBatcher;
	for (auto i = 0; i < mVertexes.Num() - 1; i++)
	{
		FVector start = FVector(mVertexes[i].X, mVertexes[i].Y, mVertexes[i].Z);
		FVector end = FVector(mVertexes[i + 1].X, mVertexes[i + 1].Y, mVertexes[i + 1].Z);
		FBatchedLine line = FBatchedLine(start,
			end,
			FLinearColor(0.28, 0.82, 0.8, 1),  //设置颜色、透明度
			0, // 设置显示时间。设为0,表示永久显示
			60,
		);
		lines.Add(line);
	}
	LineBatchComponent->BatchedLines.Empty();
	LineBatchComponent->DrawLines(lines);
}

这个函数就是把向量数组绘制成线条

然后再关卡蓝图中调用一下

UE4从零开始制作数字孪生道路监测平台

保存一下看看效果

UE4从零开始制作数字孪生道路监测平台

效果还是非常好的

出给个名字RoadArray

UE4从零开始制作数字孪生道路监测平台

在该函数内部创建一个全局变量Result,类型是一个向量数组,逻辑也很简单,遍历roadPointsLonLat数组,一个元素化成一个向量存到Result中,然后用返回节点吧Result作为参数返回出去。

UE4从零开始制作数字孪生道路监测平台

之后在车的C++中新建一个函数,也就是刚才的DrawRoadPoints

void AMyCharacterCar::DrawRoadPoints(TArray<FVector> mVertexes)
{
	TArray<FBatchedLine> lines;
	ULineBatchComponent* const LineBatchComponent = GetWorld()->PersistentLineBatcher;
	for (auto i = 0; i < mVertexes.Num() - 1; i++)
	{
		FVector start = FVector(mVertexes[i].X, mVertexes[i].Y, mVertexes[i].Z);
		FVector end = FVector(mVertexes[i + 1].X, mVertexes[i + 1].Y, mVertexes[i + 1].Z);
		FBatchedLine line = FBatchedLine(start,
			end,
			FLinearColor(0.28, 0.82, 0.8, 1),  //设置颜色、透明度
			0, // 设置显示时间。设为0,表示永久显示
			60,
		);
		lines.Add(line);
	}
	LineBatchComponent->BatchedLines.Empty();
	LineBatchComponent->DrawLines(lines);
}

这个函数就是把向量数组绘制成线条

然后再关卡蓝图中调用一下

UE4从零开始制作数字孪生道路监测平台

保存一下看看效果

UE4从零开始制作数字孪生道路监测平台

效果还是非常好的

发表回复