文章目录
- 前言
- 一、实现目标
- 二、实现过程
-
- 1.实现原理
- 2.数据读取
- 3.三角剖分
- 3.具体代码
- 4.蓝图测试
前言
UE5、CesiumForUnreal实现加载GeoJson绘制单面(Polygon)功能(StaticMesh方式)
一、实现目标
通过读取本地的Geojson数据,在UE中以staticMeshComponent的形式绘制出面数据,支持Editor和Runtime环境,如下图
<iframe id="ilMhyJ29-1705998383377" frameborder="0" src="//i2.wp.com/live.csdn.net/v/embed/361133" allowfullscreen="true" data-mediaembed="csdn"></iframe>
singlePolygon
二、实现过程
1.实现原理
首先读取Geojson数据,然后进行三角剖分,最后根据顶点和索引创建StaticMesh。
2.数据读取
本文使用的是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 ] ] } }
]
}
3.三角剖分
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 } }
3.具体代码
- AsinglePolygon_Geojson.h
// 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; };
- AsinglePolygon_Geojson.cpp
// 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(); }
代码中提到的材质,查看路径操作如下:
4.蓝图测试
- 基于c++类生成蓝图类,并放到世界场景中测试。
- 在该细节面板中配置相关设置,主要是需要CesiumGeoference,用于WGS84和UE世界坐标的转换。已经Geojson数据的存放相对路径(相对于Game工程目录),不支持Geojson多feature。如下: