UE5、CesiumForUnreal实现加载GeoJson绘制单面(Polygon)功能(StaticMesh方式)
通过读取本地的Geojson数据,在UE中以staticMeshComponent的形式绘制出面数据,支持Editor和Runtime环境,如下图
singlePolygon
首先读取Geojson数据,然后进行三角剖分,最后根据顶点和索引创建StaticMesh。
本文使用的是polygon转linestring格式的Geojson线状数据文件,特别需要注意,转完的数据需要去掉coordinates字段里的一组中括号。在UE中直接读取文本对其进行解析,生成坐标数组。本文数据只考虑一个feature情况,数据坐标格式为wgs84经纬度坐标。
例:{
“type”: “FeatureCollection”,
“name”: “singlePolygon”,
“crs”: { “type”: “name”, “properties”: { “name”: “urn:ogc:def:crs:OGC:1.3:CRS84” } },
“features”: [
{ “type”: “Feature”, “properties”: { “id”: 1 }, “geometry”: { “type”: “MultiLineString”, “coordinates”: [ [ 107.5955545517036, 34.322768426465544 ], [ 108.086216375377106, 34.660927250889173 ], [ 109.133845674571887, 34.448749164976306 ], [ 109.518418455288952, 33.261877996901205 ], [ 109.067540022724117, 32.552407522130054 ], [ 107.734796420583919, 32.738063347303815 ], [ 106.726950512497794, 32.930349737662347 ], [ 106.786625599160786, 33.792323211683374 ], [ 107.025325945812767, 33.938195645748472 ], [ 107.608815682073157, 34.322768426465544 ], [ 107.608815682073157, 34.322768426465544 ],[ 107.5955545517036, 34.322768426465544 ] ] } }
]
}
ue不支持直接绘制面,因此需要将面进行三角剖分。为快速的对地理控件点位进行三角剖分,直接使用Mapbox的earcut.hpp耳切算法三角剖分库。
地址:传送门
将其放到工程中的Source下的某个目录中,本文是放到了Developer文件夹中
Build.cs配置
using UnrealBuildTool;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
public class cesiumGeoJson : ModuleRules
{
public cesiumGeoJson(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
//PublicIncludePaths.AddRange(new string[] { Path.Combine(ModuleDirectory, "./Source/Engine/Developer") });
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" ,"CesiumRuntime","Json"});
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
// Copyright 2020-2021 CesiumGS, Inc. and Contributors
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CesiumGeoreference.h"
#include "singlePolygon_Geojson.generated.h"
UCLASS()
class CESIUMGEOJSON_API AsinglePolygon_Geojson : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AsinglePolygon_Geojson();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Current world CesiumGeoreference.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Entity | Polygon")
ACesiumGeoreference* Georeference;
// The selected feature index, current is only for '0', just for demo.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Entity | Polygon");
int SelectedFeatureIndex = 0;
// The data path, that is the relative path of ue game content.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Entity | Polygon");
FString GeoJsonPath;
/**
* @breif Test generate polygon.
*/
UFUNCTION(CallInEditor, Category = "Entity | Polygon")
void TestGeneratePolygon();
/**
* @brief Get feature vertices from linestring geojson.
*/
void GetCoordinatesFromLineStringGeoJson(const FString& FilePath, int FeatureIndex, TArray<FVector>& Positions);
/**
* @brief Build static polygon mesh component from current data.
*/
void BuildPolygonStaticMesh();
private:
// Verices that crs is unreal world.
TArray<FVector> GeometryUE;
// Vertices that crs is geographic wgs 84, epsg 4326.
TArray<FVector> GeometryGeo;
// Indices of vertices.
TArray<uint32> Indices;
};
// Copyright 2020-2021 CesiumGS, Inc. and Contributors
#include "singlePolygon_Geojson.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Engine/Developer/earcut.hpp"
#include "array"
// Sets default values
AsinglePolygon_Geojson::AsinglePolygon_Geojson()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
}
// Called when the game starts or when spawned
void AsinglePolygon_Geojson::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AsinglePolygon_Geojson::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AsinglePolygon_Geojson::TestGeneratePolygon()
{
// Check file path and georeference is exist.
if (!Georeference || GeoJsonPath.IsEmpty())
{
UE_LOG(LogTemp, Warning, TEXT("CesiumGeoreference or GeoJsonPath is valid, please check!"));
return;
}
// Get the full path of file;
FString FilePath = UKismetSystemLibrary::GetProjectDirectory() + GeoJsonPath;
GetCoordinatesFromLineStringGeoJson(FilePath, 0, GeometryGeo);
// First and last is the same point.
GeometryGeo.Pop();
// Triangulation
std::vector<std::vector<std::array<double, 2>>> Polygon;
std::vector<std::array<double, 2>> Points;
for (FVector& Item : GeometryGeo)
{
std::array<double, 2> CurPoint = { Item.X, Item.Y };
Points.push_back(CurPoint);
// Convert coord from geo to ue.
FVector PointUE = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(Item);
GeometryUE.Push(PointUE);
}
// Current is just for simply polygon.
Polygon.push_back(Points);
std::vector<uint32_t> CalculateIndices = mapbox::earcut(Polygon);
for (uint32_t Item : CalculateIndices)
{
Indices.Push(Item);
}
// Build static mesh.
BuildPolygonStaticMesh();
}
void AsinglePolygon_Geojson::GetCoordinatesFromLineStringGeoJson(
const FString& FilePath, int FeatureIndex, TArray<FVector>& Positions)
{
// Check file exist.
if (!FPaths::FileExists(FilePath)) {
UE_LOG(LogTemp, Warning, TEXT("GeoJson file don't exist!"));
return;
}
// Clear
GeometryUE.Empty();
GeometryGeo.Empty();
Indices.Empty();
FString FileString;
FFileHelper::LoadFileToString(FileString, *FilePath);
TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(FileString);
TSharedPtr<FJsonObject> Root;
// Check deserialize
if (!FJsonSerializer::Deserialize(JsonReader, Root)) {
return;
}
if (Root->HasField(TEXT("features"))) {
TArray<TSharedPtr<FJsonValue>> Features = Root->GetArrayField(TEXT("features"));
// Check feature exist
if (Features.Num() < 1 || Features.Num() < (FeatureIndex + 1)) {
return;
}
TSharedPtr<FJsonObject> Feature = Features[FeatureIndex]->AsObject();
if (Feature->HasField(TEXT("geometry"))) {
TSharedPtr<FJsonObject> Geometry = Feature->GetObjectField(TEXT("geometry"));
if (Geometry->HasField(TEXT("coordinates"))) {
TArray<TSharedPtr<FJsonValue>> Coordinates = Geometry->GetArrayField(TEXT("coordinates"));
for (auto Item : Coordinates) {
auto Coordinate = Item->AsArray();
FVector Position;
// Check coord array is 2 or 3.
if (Coordinate.Num() == 2) {
// If don't have z value, add target value for z.
Position = FVector(Coordinate[0]->AsNumber(), Coordinate[1]->AsNumber(), 5000);
}
else if (Coordinate.Num() == 3)
{
Position = FVector(Coordinate[0]->AsNumber(), Coordinate[1]->AsNumber(), Coordinate[2]->AsNumber());
}
Positions.Emplace(Position);
}
}
}
}
}
void AsinglePolygon_Geojson::BuildPolygonStaticMesh()
{
UStaticMeshComponent* pStaticMeshComponent = NewObject<UStaticMeshComponent>(this);
pStaticMeshComponent->SetFlags(RF_Transient);
pStaticMeshComponent->SetWorldLocationAndRotation(FVector(0, 0, 0), FRotator(0, 0, 0));
UStaticMesh* pStaticMesh = NewObject<UStaticMesh>(pStaticMeshComponent);
pStaticMesh->NeverStream = true;
pStaticMeshComponent->SetStaticMesh(pStaticMesh);
FStaticMeshRenderData* pRenderData = new FStaticMeshRenderData();
pRenderData->AllocateLODResources(1);
FStaticMeshLODResources& LODResourece = pRenderData->LODResources[0];
TArray<FStaticMeshBuildVertex> StaticMeshBuildVertices;
StaticMeshBuildVertices.SetNum(GeometryUE.Num());
// Calculate bounds
glm::dvec3 MinPosition{ std::numeric_limits<double>::max() };
glm::dvec3 MaxPosition{ std::numeric_limits<double>::lowest() };
// Vertices
for (int i = 0; i < GeometryUE.Num(); i++)
{
FStaticMeshBuildVertex& Vertex = StaticMeshBuildVertices[i];
Vertex.Position = FVector3f(GeometryUE[i]);
Vertex.UVs[0] = FVector2f(0, 0);
Vertex.TangentX = FVector3f(0, 1, 0);
Vertex.TangentY = FVector3f(1, 0, 0);
Vertex.TangentZ = FVector3f(0, 0, 1);
// Calculate max and min position;
MinPosition.x = glm::min<double>(MinPosition.x, GeometryUE[i].X);
MinPosition.y = glm::min<double>(MinPosition.y, GeometryUE[i].Y);
MinPosition.z = glm::min<double>(MinPosition.z, GeometryUE[i].Z);
MaxPosition.x = glm::max<double>(MaxPosition.x, GeometryUE[i].X);
MaxPosition.y = glm::max<double>(MaxPosition.y, GeometryUE[i].Y);
MaxPosition.z = glm::max<double>(MaxPosition.z, GeometryUE[i].Z);
}
// Bounding box
FBox BoundingBox(FVector3d(MinPosition.x, MinPosition.y, MinPosition.z), FVector3d(MaxPosition.x, MaxPosition.y, MaxPosition.z));
BoundingBox.GetCenterAndExtents(pRenderData->Bounds.Origin, pRenderData->Bounds.BoxExtent);
LODResourece.bHasColorVertexData = false;
LODResourece.VertexBuffers.PositionVertexBuffer.Init(StaticMeshBuildVertices);
LODResourece.VertexBuffers.StaticMeshVertexBuffer.Init(StaticMeshBuildVertices, 1);
LODResourece.IndexBuffer.SetIndices(Indices, EIndexBufferStride::AutoDetect);
LODResourece.bHasDepthOnlyIndices = false;
LODResourece.bHasReversedIndices = false;
LODResourece.bHasReversedDepthOnlyIndices = false;
FStaticMeshSectionArray& Sections = LODResourece.Sections;
FStaticMeshSection& Section = Sections.AddDefaulted_GetRef();
Section.bEnableCollision = true;
Section.bCastShadow = true;
Section.NumTriangles = Indices.Num() / 3;
Section.FirstIndex = 0;
Section.MinVertexIndex = 0;
Section.MaxVertexIndex = Indices.Num() - 1;
// Add material
UMaterialInterface* CurMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("Material'/Game/Martials/M_Polygon.M_Polygon'")); //此处的材质需要手动在编辑器中创建,而后在c++代码中引用
UMaterialInstanceDynamic* CurMaterialIns = UMaterialInstanceDynamic::Create(CurMaterial, nullptr);
CurMaterialIns->AddToRoot();
CurMaterialIns->TwoSided = true;
FName CurMaterialSlotName = pStaticMesh->AddMaterial(CurMaterialIns);
int32 CurMaterialIndex = pStaticMesh->GetMaterialIndex(CurMaterialSlotName);
Section.MaterialIndex = CurMaterialIndex;
// Todo:Build Collision
pStaticMesh->SetRenderData(TUniquePtr<FStaticMeshRenderData>(pRenderData));
pStaticMesh->InitResources();
pStaticMesh->CalculateExtendedBounds();
pRenderData->ScreenSize[0].Default = 1.0f;
pStaticMesh->CreateBodySetup();
pStaticMeshComponent->SetMobility(EComponentMobility::Movable);
pStaticMeshComponent->SetupAttachment(this->RootComponent);
pStaticMeshComponent->RegisterComponent();
}
代码中提到的材质,查看路径操作如下:
- 基于c++类生成蓝图类,并放到世界场景中测试。
- 在该细节面板中配置相关设置,主要是需要CesiumGeoference,用于WGS84和UE世界坐标的转换。已经Geojson数据的存放相对路径(相对于Game工程目录),不支持Geojson多feature。如下: