├── Source ├── LandscapeGenEditor │ ├── Private │ │ ├── GISData.cpp │ │ ├── GISDataSource.cpp │ │ ├── LandscapeGenerator.cpp │ │ ├── LandscapeConstraints.cpp │ │ ├── LandscapeGeneration.cpp │ │ ├── AsyncDataRetrieval.cpp │ │ └── LandscapeGenerationBPFL.cpp │ ├── Public │ │ ├── LandscapeGeneration.h │ │ ├── LandscapeGenerator.h │ │ ├── GISDataSource.h │ │ ├── LandscapeConstraints.h │ │ ├── LandscapeGenerationBPFL.h │ │ ├── AsyncDataRetrieval.h │ │ └── GISData.h │ └── LandscapeGenEditor.Build.cs ├── GDALDataSource │ ├── Public │ │ ├── GDALDataSourceModule.h │ │ └── GDALDataSource.h │ ├── Private │ │ ├── GDALDataSourceModule.cpp │ │ └── GDALDataSource.cpp │ └── GDALDataSource.Build.cs ├── MapboxDataSource │ ├── Public │ │ ├── MapboxDataSourceModule.h │ │ └── MapboxDataSource.h │ ├── Private │ │ ├── MapboxDataSourceModule.cpp │ │ └── MapboxDataSource.cpp │ └── MapboxDataSource.Build.cs └── LandscapeGenRuntime │ ├── Public │ ├── LandscapeGenRuntime.h │ └── GISDataComponent.h │ ├── Private │ ├── LandscapeGenRuntime.cpp │ └── GISDataComponent.cpp │ └── LandscapeGenRuntime.Build.cs ├── Resources ├── Icon128.png └── architecture.drawio ├── Content └── Examples │ ├── SimpleGdalEUW.uasset │ ├── SimpleMapboxEUW.uasset │ └── SimpleGetLocationEUW.uasset ├── LandscapeGen.uplugin ├── LICENSE ├── .gitignore └── README.md /Source/LandscapeGenEditor/Private/GISData.cpp: -------------------------------------------------------------------------------- 1 | #include "GISData.h" 2 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Private/GISDataSource.cpp: -------------------------------------------------------------------------------- 1 | #include "GISDataSource.h" 2 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/LandscapeGen/HEAD/Resources/Icon128.png -------------------------------------------------------------------------------- /Content/Examples/SimpleGdalEUW.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/LandscapeGen/HEAD/Content/Examples/SimpleGdalEUW.uasset -------------------------------------------------------------------------------- /Content/Examples/SimpleMapboxEUW.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/LandscapeGen/HEAD/Content/Examples/SimpleMapboxEUW.uasset -------------------------------------------------------------------------------- /Content/Examples/SimpleGetLocationEUW.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TensorWorks/LandscapeGen/HEAD/Content/Examples/SimpleGetLocationEUW.uasset -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Private/LandscapeGenerator.cpp: -------------------------------------------------------------------------------- 1 | #include "LandscapeGenerator.h" 2 | 3 | ALandscape* ULandscapeGenerator::GenerateLandscapeFromGISData(const UObject* WorldContext, const FGISData& GISData, const FVector& Scale3D) { 4 | return nullptr; 5 | } 6 | -------------------------------------------------------------------------------- /Source/GDALDataSource/Public/GDALDataSourceModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Modules/ModuleManager.h" 5 | 6 | class FGDALDataSourceModule : public IModuleInterface 7 | { 8 | public: 9 | virtual void StartupModule() override; 10 | virtual void ShutdownModule() override; 11 | }; 12 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Public/LandscapeGeneration.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Modules/ModuleManager.h" 5 | 6 | class FLandscapeGenerationModule : public IModuleInterface 7 | { 8 | public: 9 | virtual void StartupModule() override; 10 | virtual void ShutdownModule() override; 11 | }; 12 | -------------------------------------------------------------------------------- /Source/MapboxDataSource/Public/MapboxDataSourceModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Modules/ModuleManager.h" 5 | 6 | class FMapboxDataSourceModule : public IModuleInterface 7 | { 8 | public: 9 | virtual void StartupModule() override; 10 | virtual void ShutdownModule() override; 11 | }; 12 | -------------------------------------------------------------------------------- /Source/LandscapeGenRuntime/Public/LandscapeGenRuntime.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Modules/ModuleManager.h" 5 | 6 | class FLandscapeGenRuntimeModule : public IModuleInterface 7 | { 8 | public: 9 | virtual void StartupModule() override; 10 | virtual void ShutdownModule() override; 11 | }; 12 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Private/LandscapeConstraints.cpp: -------------------------------------------------------------------------------- 1 | #include "LandscapeConstraints.h" 2 | 3 | // The current implementation of the landscape generation system is limited by the maximum 4 | // size of a single texture on the GPU, which is 16384x16384 pixels under most Unreal RHIs 5 | 6 | uint64 LandscapeConstraints::MaxRasterSizeX() { 7 | return 16384; 8 | } 9 | 10 | uint64 LandscapeConstraints::MaxRasterSizeY() { 11 | return LandscapeConstraints::MaxRasterSizeX(); 12 | } 13 | -------------------------------------------------------------------------------- /Source/GDALDataSource/Private/GDALDataSourceModule.cpp: -------------------------------------------------------------------------------- 1 | #include "GDALDataSourceModule.h" 2 | #include "UnrealGDAL.h" 3 | 4 | #define LOCTEXT_NAMESPACE "FGDALDataSourceModule" 5 | 6 | void FGDALDataSourceModule::StartupModule() 7 | { 8 | FUnrealGDALModule* UnrealGDAL = FModuleManager::Get().LoadModulePtr("UnrealGDAL"); 9 | UnrealGDAL->InitGDAL(); 10 | } 11 | 12 | void FGDALDataSourceModule::ShutdownModule() {} 13 | 14 | #undef LOCTEXT_NAMESPACE 15 | 16 | IMPLEMENT_MODULE(FGDALDataSourceModule, GDALDataSource) -------------------------------------------------------------------------------- /Source/MapboxDataSource/Private/MapboxDataSourceModule.cpp: -------------------------------------------------------------------------------- 1 | #include "MapboxDataSourceModule.h" 2 | #include "UnrealGDAL.h" 3 | 4 | #define LOCTEXT_NAMESPACE "FMapboxDataSourceModule" 5 | 6 | void FMapboxDataSourceModule::StartupModule() 7 | { 8 | FUnrealGDALModule* UnrealGDAL = FModuleManager::Get().LoadModulePtr("UnrealGDAL"); 9 | UnrealGDAL->InitGDAL(); 10 | } 11 | 12 | void FMapboxDataSourceModule::ShutdownModule() {} 13 | 14 | #undef LOCTEXT_NAMESPACE 15 | 16 | IMPLEMENT_MODULE(FMapboxDataSourceModule, MapboxDataSource) -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Private/LandscapeGeneration.cpp: -------------------------------------------------------------------------------- 1 | #include "LandscapeGeneration.h" 2 | #include "UnrealGDAL.h" 3 | 4 | #define LOCTEXT_NAMESPACE "FLandscapeGenerationModule" 5 | 6 | void FLandscapeGenerationModule::StartupModule() 7 | { 8 | FUnrealGDALModule* UnrealGDAL = FModuleManager::Get().LoadModulePtr("UnrealGDAL"); 9 | UnrealGDAL->InitGDAL(); 10 | } 11 | 12 | void FLandscapeGenerationModule::ShutdownModule() {} 13 | 14 | #undef LOCTEXT_NAMESPACE 15 | 16 | IMPLEMENT_MODULE(FLandscapeGenerationModule, LandscapeGenEditor) -------------------------------------------------------------------------------- /Source/LandscapeGenRuntime/Private/LandscapeGenRuntime.cpp: -------------------------------------------------------------------------------- 1 | #include "LandscapeGenRuntime.h" 2 | #include "UnrealGDAL.h" 3 | 4 | #define LOCTEXT_NAMESPACE "FLandscapeGenRuntimeModule" 5 | 6 | void FLandscapeGenRuntimeModule::StartupModule() 7 | { 8 | FUnrealGDALModule* UnrealGDAL = FModuleManager::Get().LoadModulePtr("UnrealGDAL"); 9 | UnrealGDAL->InitGDAL(); 10 | } 11 | 12 | void FLandscapeGenRuntimeModule::ShutdownModule() {} 13 | 14 | #undef LOCTEXT_NAMESPACE 15 | 16 | IMPLEMENT_MODULE(FLandscapeGenRuntimeModule, LandscapeGenRuntime) -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Public/LandscapeGenerator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Kismet/BlueprintFunctionLibrary.h" 5 | #include "GISData.h" 6 | #include "LandscapeGenerator.generated.h" 7 | 8 | UCLASS() 9 | class LANDSCAPEGENEDITOR_API ULandscapeGenerator : public UBlueprintFunctionLibrary 10 | { 11 | GENERATED_BODY() 12 | 13 | public: 14 | 15 | UFUNCTION(BlueprintCallable, Category = "LandscapeGenerator|Single") 16 | static ALandscape* GenerateLandscapeFromGISData(const UObject* WorldContext, const FGISData& GISData, const FVector& Scale3D); 17 | }; 18 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Public/GISDataSource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "GISData.h" 5 | #include "GISDataSource.generated.h" 6 | 7 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGISDataSourceDelegate, const FString&, Error, const FGISData&, Data); 8 | 9 | UINTERFACE(Blueprintable) 10 | class UGISDataSource : public UInterface 11 | { 12 | GENERATED_BODY() 13 | }; 14 | 15 | class LANDSCAPEGENEDITOR_API IGISDataSource 16 | { 17 | GENERATED_BODY() 18 | 19 | public: 20 | 21 | virtual void RetrieveData(FGISDataSourceDelegate OnSuccess, FGISDataSourceDelegate OnFailure) = 0; 22 | }; 23 | -------------------------------------------------------------------------------- /Source/LandscapeGenRuntime/LandscapeGenRuntime.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class LandscapeGenRuntime : ModuleRules 4 | { 5 | public LandscapeGenRuntime(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 8 | 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core" 13 | } 14 | ); 15 | 16 | PrivateDependencyModuleNames.AddRange( 17 | new string[] 18 | { 19 | "CoreUObject", 20 | "Engine", 21 | "Landscape", 22 | "UnrealGDAL", 23 | "GDAL" 24 | } 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Public/LandscapeConstraints.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | 5 | class LandscapeConstraints 6 | { 7 | public: 8 | 9 | // Determines the maximum supported width in pixels of raster data used to generate landscapes 10 | // (Returns zero if there is no limit in the current environment) 11 | static LANDSCAPEGENEDITOR_API uint64 MaxRasterSizeX(); 12 | 13 | // Determines the maximum supported height in pixels of raster data used to generate landscapes 14 | // (Returns zero if there is no limit in the current environment) 15 | static LANDSCAPEGENEDITOR_API uint64 MaxRasterSizeY(); 16 | }; 17 | -------------------------------------------------------------------------------- /Source/GDALDataSource/GDALDataSource.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class GDALDataSource : ModuleRules 4 | { 5 | public GDALDataSource(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 8 | 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core", 13 | "GDAL", 14 | "UnrealGDAL", 15 | "LandscapeGenEditor" 16 | } 17 | ); 18 | 19 | PrivateDependencyModuleNames.AddRange( 20 | new string[] 21 | { 22 | "CoreUObject", 23 | "Engine", 24 | "Slate", 25 | "SlateCore" 26 | } 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Private/AsyncDataRetrieval.cpp: -------------------------------------------------------------------------------- 1 | #include "AsyncDataRetrieval.h" 2 | 3 | UAsyncDataRetrieval* UAsyncDataRetrieval::RetrieveDataFromSource(const TScriptInterface& DataSource) 4 | { 5 | UAsyncDataRetrieval* retriever = NewObject(); 6 | retriever->DataSource = DataSource; 7 | return retriever; 8 | } 9 | 10 | void UAsyncDataRetrieval::Activate() { 11 | this->DataSource->RetrieveData(this->OnSuccess, this->OnFailure); 12 | } 13 | 14 | UAsyncDataRetrieval::UAsyncDataRetrieval(const FObjectInitializer& ObjectInitializer) 15 | : Super(ObjectInitializer) 16 | { 17 | if ( HasAnyFlags(RF_ClassDefaultObject) == false ) 18 | { 19 | AddToRoot(); 20 | } 21 | } -------------------------------------------------------------------------------- /Source/MapboxDataSource/MapboxDataSource.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class MapboxDataSource : ModuleRules 4 | { 5 | public MapboxDataSource(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 8 | 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core", 13 | "Http", 14 | "Json", 15 | "JsonUtilities", 16 | "RHI", 17 | "RenderCore", 18 | "LandscapeGenEditor", 19 | "UnrealGDAL", 20 | "GDAL" 21 | } 22 | ); 23 | 24 | PrivateDependencyModuleNames.AddRange( 25 | new string[] 26 | { 27 | "CoreUObject", 28 | "Engine", 29 | "Slate", 30 | "SlateCore" 31 | } 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/LandscapeGenEditor.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class LandscapeGenEditor : ModuleRules 4 | { 5 | public LandscapeGenEditor(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 8 | 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core", 13 | "LandscapeGenRuntime" 14 | } 15 | ); 16 | 17 | PrivateDependencyModuleNames.AddRange( 18 | new string[] 19 | { 20 | "CoreUObject", 21 | "Engine", 22 | "Slate", 23 | "SlateCore", 24 | "Landscape", 25 | "Foliage", 26 | "UnrealEd", 27 | "RHI", 28 | "RenderCore", 29 | "UnrealGDAL", 30 | "GDAL" 31 | } 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Public/LandscapeGenerationBPFL.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Kismet/BlueprintFunctionLibrary.h" 5 | #include "GISData.h" 6 | #include "LandscapeGenerationBPFL.generated.h" 7 | 8 | UCLASS() 9 | class LANDSCAPEGENEDITOR_API ULandscapeGenerationBPFL : public UBlueprintFunctionLibrary 10 | { 11 | GENERATED_BODY() 12 | 13 | UFUNCTION(BlueprintCallable, Category = "LandscapeGen|Single") 14 | static ALandscape* GenerateLandscapeFromGISData( 15 | const UObject* WorldContext, const FString& LandscapeName, const FGISData& GISData, const FVector& Scale3D 16 | ); 17 | 18 | UFUNCTION(BlueprintCallable, Category = "LandscapeGen|Utils") 19 | static UMaterial* GenerateUnlitLandscapeMaterial( 20 | const FString& LandscapeName, const FString& TexturePath, const int32& NumComponentsX, 21 | const int32& NumComponentsY, const int32& NumQuads 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Public/AsyncDataRetrieval.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Interfaces/IHttpRequest.h" 5 | #include "UObject/ObjectMacros.h" 6 | #include "Kismet/BlueprintAsyncActionBase.h" 7 | #include "GISDataSource.h" 8 | #include "AsyncDataRetrieval.generated.h" 9 | 10 | UCLASS(Blueprintable) 11 | class LANDSCAPEGENEDITOR_API UAsyncDataRetrieval : public UBlueprintAsyncActionBase 12 | { 13 | GENERATED_UCLASS_BODY() 14 | 15 | public: 16 | 17 | UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" )) 18 | static UAsyncDataRetrieval* RetrieveDataFromSource(const TScriptInterface& DataSource); 19 | 20 | UPROPERTY(BlueprintAssignable) 21 | FGISDataSourceDelegate OnSuccess; 22 | 23 | UPROPERTY(BlueprintAssignable) 24 | FGISDataSourceDelegate OnFailure; 25 | 26 | virtual void Activate(); 27 | 28 | private: 29 | TScriptInterface DataSource; 30 | }; 31 | -------------------------------------------------------------------------------- /Source/GDALDataSource/Public/GDALDataSource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "GISDataSource.h" 5 | #include "GDALDataSource.generated.h" 6 | 7 | UCLASS(Blueprintable) 8 | class GDALDATASOURCE_API UGDALDataSource : public UObject, public IGISDataSource 9 | { 10 | GENERATED_BODY() 11 | 12 | public: 13 | 14 | // Attempts to retrieve the GIS data from the specified heightmap and RGB datasets 15 | virtual void RetrieveData(FGISDataSourceDelegate OnSuccess, FGISDataSourceDelegate OnFailure); 16 | 17 | // The path to the GDAL raster dataset containing the heightmap data 18 | UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn="true")) 19 | FString HeightmapDataset; 20 | 21 | // The path to the GDAL raster dataset containing the RGB data 22 | UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn="true")) 23 | FString RGBDataset; 24 | 25 | private: 26 | FString RetrieveDataInternal(FGISData& data); 27 | }; 28 | -------------------------------------------------------------------------------- /LandscapeGen.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "0.0.1", 5 | "FriendlyName": "LandscapeGen", 6 | "Description": "Generate landscapes from GIS data inside the Unreal Editor", 7 | "Category": "Other", 8 | "CreatedBy": "TensorWorks Pty Ltd", 9 | "CreatedByURL": "https://tensorworks.com.au", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "EngineVersion": "4.25.0", 14 | "CanContainContent": true, 15 | "IsBetaVersion": false, 16 | "IsExperimentalVersion": false, 17 | "Installed": false, 18 | "Modules": [ 19 | { 20 | "Name": "LandscapeGenRuntime", 21 | "Type": "Runtime", 22 | "LoadingPhase": "Default" 23 | }, 24 | { 25 | "Name": "LandscapeGenEditor", 26 | "Type": "Editor", 27 | "LoadingPhase": "Default" 28 | }, 29 | { 30 | "Name": "GDALDataSource", 31 | "Type": "Editor", 32 | "LoadingPhase": "Default" 33 | }, 34 | { 35 | "Name": "MapboxDataSource", 36 | "Type": "Editor", 37 | "LoadingPhase": "Default" 38 | } 39 | ], 40 | "Plugins": [ 41 | { 42 | "Name": "UnrealGDAL", 43 | "Enabled": true 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TensorWorks Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Visual Studio 2015 database file 5 | *.VC.db 6 | 7 | # Compiled Object files 8 | *.slo 9 | *.lo 10 | *.o 11 | *.obj 12 | 13 | # Precompiled Headers 14 | *.gch 15 | *.pch 16 | 17 | # Compiled Dynamic libraries 18 | *.so 19 | *.dylib 20 | *.dll 21 | 22 | # Fortran module files 23 | *.mod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.ipa 36 | 37 | # These project files can be generated by the engine 38 | *.xcodeproj 39 | *.xcworkspace 40 | *.sln 41 | *.suo 42 | *.opensdf 43 | *.sdf 44 | *.VC.db 45 | *.VC.opendb 46 | 47 | # Linux generated project files 48 | .idea/ 49 | .kdev4/ 50 | .vscode/ 51 | *.pri 52 | *.code-workspace 53 | *.kdev4 54 | *.pro 55 | *.workspace 56 | Makefile 57 | CMakeLists.txt 58 | *CodeCompletionFolders.txt 59 | *CodeLitePreProcessor.txt 60 | 61 | # Precompiled Assets 62 | SourceArt/**/*.png 63 | SourceArt/**/*.tga 64 | 65 | # Binary Files 66 | Binaries/* 67 | Plugins/*/Binaries/* 68 | 69 | # Packaged builds 70 | dist/* 71 | 72 | # Builds 73 | Build/* 74 | 75 | # Whitelist PakBlacklist-.txt files 76 | !Build/*/ 77 | Build/*/** 78 | !Build/*/PakBlacklist*.txt 79 | 80 | # Don't ignore icon files in Build 81 | !Build/**/*.ico 82 | 83 | # Built data for maps 84 | *_BuiltData.uasset 85 | 86 | # Configuration files generated by the Editor 87 | Saved/* 88 | 89 | # Compiled source files for the engine to use 90 | Intermediate/* 91 | Plugins/*/Intermediate/* 92 | 93 | # Cache files for the editor to use 94 | DerivedDataCache/* 95 | -------------------------------------------------------------------------------- /Source/LandscapeGenRuntime/Public/GISDataComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Components/SceneComponent.h" 5 | #include "GDALHelpers.h" 6 | #include "GISDataComponent.generated.h" 7 | 8 | UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) 9 | class LANDSCAPEGENRUNTIME_API UGISDataComponent : public USceneComponent 10 | { 11 | GENERATED_BODY() 12 | 13 | public: 14 | 15 | // Sets default values for this component's properties 16 | UGISDataComponent(); 17 | 18 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 19 | int NumComponentsX; 20 | 21 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 22 | int NumComponentsY; 23 | 24 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 25 | int ComponentSizeQuads; 26 | 27 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 28 | int NumPixelsX; 29 | 30 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 31 | int NumPixelsY; 32 | 33 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 34 | FVector2D UpperLeft; 35 | 36 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 37 | FVector2D LowerRight; 38 | 39 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 40 | FString WKT; 41 | 42 | protected: 43 | 44 | // Called when the game starts 45 | virtual void BeginPlay() override; 46 | 47 | public: 48 | void SetGeoTransforms(GDALDatasetRef& GPSCoordinate); 49 | 50 | UFUNCTION(BlueprintCallable, CallInEditor) 51 | FVector GetWorldSpaceLocation(FVector2D GPSCoordinate); 52 | 53 | UFUNCTION(BlueprintCallable, CallInEditor) 54 | FVector2D GetGPSLocation(const FVector& WorldSpaceCoordinate); 55 | 56 | private: 57 | UPROPERTY() 58 | TArray GeoTransform; 59 | 60 | UPROPERTY() 61 | TArray InvGeoTransform; 62 | }; 63 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Public/GISData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Math/Vector2D.h" 5 | #include "GISData.generated.h" 6 | 7 | 8 | UENUM(BlueprintType) 9 | enum ECornerCoordinateType 10 | { 11 | // Corner coordinates are WGS84 (lat,lon) values 12 | LatLon UMETA(DisplayName = "LatLon"), 13 | 14 | // Corner coordinates are in the projected coordinate system of the raster data 15 | Projected UMETA(DisplayName = "Projected"), 16 | }; 17 | 18 | 19 | USTRUCT(BlueprintType) 20 | struct LANDSCAPEGENEDITOR_API FGISData 21 | { 22 | GENERATED_BODY() 23 | 24 | // The buffer of raw heightmap values (in metres) and the heightmap raster dimensions 25 | UPROPERTY(BlueprintReadWrite) 26 | TArray HeightBuffer; 27 | uint32 HeightBufferX, HeightBufferY; 28 | 29 | // The buffer of colour values and the colour raster dimensions 30 | UPROPERTY(BlueprintReadWrite) 31 | TArray ColorBuffer; 32 | uint32 ColorBufferX, ColorBufferY; 33 | 34 | UPROPERTY(BlueprintReadWrite) 35 | TEnumAsByte PixelFormat = EPixelFormat::PF_B8G8R8A8; 36 | 37 | // The Well-Known Text (WKT) representation of the projected coordinate system used by the raster data 38 | UPROPERTY(BlueprintReadWrite) 39 | FString ProjectionWKT; 40 | 41 | // Specifies whether the corner coordinates are WGS84 (lat,lon) or projected coordinates 42 | UPROPERTY(BlueprintReadWrite) 43 | TEnumAsByte CornerType = ECornerCoordinateType::LatLon; 44 | 45 | // The coordinate of the upper-left corner of the raster data 46 | UPROPERTY(BlueprintReadWrite) 47 | FVector2D UpperLeft; 48 | 49 | // The coordinate of the lower-right corner of the raster data 50 | UPROPERTY(BlueprintReadWrite) 51 | FVector2D LowerRight; 52 | }; 53 | -------------------------------------------------------------------------------- /Source/MapboxDataSource/Public/MapboxDataSource.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Interfaces/IHttpRequest.h" 5 | #include "GISDataSource.h" 6 | #include "UObject/NoExportTypes.h" 7 | #include "MapboxDataSource.generated.h" 8 | 9 | enum class EMapboxRequestDataType 10 | { 11 | RGB, 12 | HEIGHT 13 | }; 14 | 15 | struct FMapboxRequestData 16 | { 17 | EMapboxRequestDataType DataType; 18 | int RelX; 19 | int RelY; 20 | }; 21 | 22 | UCLASS(Blueprintable) 23 | class MAPBOXDATASOURCE_API UMapboxDataSource : public UObject, public IGISDataSource 24 | { 25 | GENERATED_BODY() 26 | 27 | public: 28 | const static FString ProjectionWKT; 29 | 30 | virtual void RetrieveData(FGISDataSourceDelegate OnSuccess, FGISDataSourceDelegate OnFailure); 31 | 32 | void RequestSectionRGBHeight(float upperLat, float leftLon, float lowerLat, float rightLon, int zoom); 33 | 34 | UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = "true")) 35 | FString reqAPIKey; 36 | 37 | UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = "true")) 38 | float reqUpperLat; 39 | 40 | UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = "true")) 41 | float reqLeftLon; 42 | 43 | UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = "true")) 44 | float reqLowerLat; 45 | 46 | UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = "true")) 47 | float reqRightLon; 48 | 49 | UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = "true")) 50 | int reqZoom; 51 | 52 | UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn = "true")) 53 | bool bIgnoreMissingTiles; 54 | 55 | FGISDataSourceDelegate OnSuccess; 56 | 57 | FGISDataSourceDelegate OnFailure; 58 | 59 | void Start(FString URL, FMapboxRequestData data); 60 | 61 | TArray HeightData; 62 | TArray RGBData; 63 | 64 | private: 65 | bool hasReqFailed = false; 66 | 67 | int DimX; 68 | int DimY; 69 | int TileDimX; 70 | int TileDimY; 71 | int OffsetX; 72 | int OffsetY; 73 | int Zoom; 74 | int MaxX; 75 | int MaxY; 76 | float MinU; 77 | float MaxU; 78 | float MinV; 79 | float MaxV; 80 | int TotalRequests; 81 | int CompletedRequests; 82 | int NumXHeightPixels; 83 | int NumYHeightPixels; 84 | 85 | bool ValidateRequest(FString& OutError); 86 | void HandleMapboxRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded, FMapboxRequestData data); 87 | }; 88 | -------------------------------------------------------------------------------- /Resources/architecture.drawio: -------------------------------------------------------------------------------- 1 | 7VvbcuI4EP0aqmYeQsnylccQcqvNVKVCtmZnX7aELcAZY3llEWC/flqyfMMMYQiYJDt+wWpJLXXr6KglmY55MVtec5JMv7CARh2MgmXHHHQwNgzXgx8pWWUSx3UywYSHgS5UCobhf1QLkZbOw4CmtYKCsUiESV3oszimvqjJCOdsUS82ZlG91YRMaEMw9EnUlH4NAzHN7XJ6ZcYNDSdT3bSH3SxjRvLC2pJ0SgK2qIjMy455wRkT2dtseUEj6bzcL1m9q5/kFh3jNBa7VHi68UmQ3NDl/dPfSxM9Jm7yz5mpzXgm0VxbfH07BMGACAI/F2yWsFi2gJ0ImumPOLxN5FsuCaVtYqUd5vw7lwb1xywWZ6kaznMoYKBkWWauqyiVDgXjMNww7pSlCREhkSbMqCBB1iEyYnMhf1WZmHIiaADvEYmD1CcJlXmxlCScPSvsqPpTFqRq/HkGlmfKRRhPIDGiYkFpDG9fr4eeBb93BFq4umOxKsp4EMbQSlpo/jPmVPXrMp6EsWxxwXgkc6DLPq3X6uZGgsqwNBzXvIY5m8cBlSNlQPZiGgo6VLrMwQImFsimYhbpbNmK7DuknEJZFQgaG9JGuqyINDCuKQOP8BUU0bm2qUGqZylGOr0oMY9tLZtW4G7k85ToeTYpdJdQhBeNxl9ApoWayKyM91053m/LlZ77sisN1K4rjYYrK+5D2q2hwnv//uqupbkuy46gRwkPJb+g8Tz2dS+icMSJ8uon2aHPkvWnclYWcz7dOA8LFlBznbOZNK/KZ2z0BKvE+5iUrou6yKyBydqApV6v23NbhZO1dc1oaZ2AkSbAsaliAw4goim4OocxGxcjrxcOWA6Cua9Kj1Z6AalBY8jm3Kdd+ZYvQtqfhY6M/h+u+4XoQkp9H1ZJEoel8p+tXp+gFxKAWSe//vGYafAZB1SvrzYyhwq/+/k9gNVCu4AVO+0Sn709usmGvCXI9kkqKQq4jvJxFiaEsySiM3B6BZbKKg2XVPUvLdlPg/glVmta8pahU6yGW3BjtIsbp+EjGsC2QCcZh3BywmISXZbSfulFBKmyzB1jifbdExVipfc4ZC5Y3bN0GYq/ZPWua+vkt0rWYKlVq8QqT8RgcFbLzpPfqnllNZXK66WCs+/0gkUQEEsDTQTP1ZWG95ocoW0jnaF0h/hDED6hYoeYTzp7K3I4jYBdn+t7tE0w0FXvWRZi5DEarJn5Bk6DznDXwJRZpStWt1UbdLnI6hVPPfpbV5v5oKFWwbQw8BXIdX8aNacNTMP8FHUIkiicxPDuS04CAPTVNgl2w+c6YxYGQYZ2CgxIRkqVREciLVI22v2OPQBJREY06hP/+0TNjAqkxur5JahtnaYNqimOA3QHO9Ud9yYKgrXLwlZt5M60pn2hlhdh43FKjzPW3mlZCu/FUpaHqzx1Bq43egcnK1X/nvIQnCyBPGiJwfLzlKMzmAVRf+Xx6qyD9iSznmEVmwmty7RbZrANJ1Ifl8G8/zWD5dA6DYMZFfrS0dMOBFYLssqY6w2EWTn3vJkwq+fZXYRLkqoTi+HsSVIGsusbBnN9I3BkirKb52nnQhB/qrZxALePRlO5v98kTWnNZ063t1FvCzSGT0JjrVHSPoT5iljL2pXGjHZozHa2xVrr4dGuLObi3nqo1XKkZZsNGrtNsymdzmfFkdRH4zJ8OC7DyDgOlxl213NOxWbWbzY7IJvZu7KZ1Q6bYVPyDioebK/hbE8+Mx20XdGx2ax51n+fnZZ/vG1jPkUPwWGGjbzOUTjM6Lr4VBy24Rh0cH7XOc3NzwMl6uMTfauj76XHYaSudtRVoJiq62vmqxtDlbVKBZXl5mn2nYo04J3e8Fh1atj4SUS7X5fYXgMgX0gyYsuTQUTwkD7TJkwyZBSde7gcPsLP+f3tO8VCcZW3DQz5qtkOGByj4aRWI576zV2Rd/CIp4xyihvCI0Q8+dR6OeKx24p41o6LrD1DHGzjbj1aatwaHjvI6f1G6gGR6r4xpBryI5rKg+sf1ODe3iem2/WuXxcdGcX5TqF68JB/CfMBg/X8QuvVwbq8K/XWT0/xQaJ1G3W9OijO3BpkEDpA8A7J8nv3rHj5rwHz8gc= -------------------------------------------------------------------------------- /Source/LandscapeGenRuntime/Private/GISDataComponent.cpp: -------------------------------------------------------------------------------- 1 | #include "GISDataComponent.h" 2 | #include "Landscape.h" 3 | 4 | // Sets default values for this component's properties 5 | UGISDataComponent::UGISDataComponent() 6 | { 7 | // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features 8 | // off to improve performance if you don't need them. 9 | PrimaryComponentTick.bCanEverTick = false; 10 | } 11 | 12 | // Called when the game starts 13 | void UGISDataComponent::BeginPlay() 14 | { 15 | Super::BeginPlay(); 16 | } 17 | 18 | void UGISDataComponent::SetGeoTransforms(GDALDatasetRef& DatasetRef) 19 | { 20 | GeoTransform.AddZeroed(6); 21 | FMemory::Memcpy(GeoTransform.GetData(), GDALHelpers::GetGeoTransform(DatasetRef).Get(), 6 * sizeof(double)); 22 | 23 | InvGeoTransform.AddZeroed(6); 24 | FMemory::Memcpy(InvGeoTransform.GetData(), GDALHelpers::GetInvertedGeoTransform(DatasetRef).Get(), 6 * sizeof(double)); 25 | } 26 | 27 | FVector UGISDataComponent::GetWorldSpaceLocation(FVector2D GPSCoordinate) 28 | { 29 | // Get Transform from lat long 30 | FString WGS84_WKT = GDALHelpers::WktFromEPSG(4326); 31 | OGRCoordinateTransformationRef CoordTransform = GDALHelpers::CreateCoordinateTransform(WGS84_WKT, WKT); 32 | 33 | UE_LOG(LogTemp, Log, TEXT("coord %s"), *GPSCoordinate.ToString()); 34 | 35 | UE_LOG(LogTemp, Log, TEXT("WKT:\n %s"), *WKT); 36 | 37 | // LatLong to Projection 38 | FVector Projected; 39 | 40 | // Prior to GDAL 3.0, coordinate transformations expect WGS84 coordinates to be in (lon,lat) format instead of (lat,lon) 41 | // (See: https://gdal.org/tutorials/osr_api_tut.html#crs-and-axis-order) 42 | #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0) 43 | GPSCoordinate = FVector2D(GPSCoordinate.Y, GPSCoordinate.X); 44 | #endif 45 | 46 | check(GDALHelpers::TransformCoordinate(CoordTransform, FVector(GPSCoordinate, 0), Projected)) 47 | 48 | // Projection to pixel space 49 | FVector2D PixelLoc = GDALHelpers::ApplyGeoTransform(InvGeoTransform.GetData(), FVector2D(Projected)); 50 | 51 | // Get Height at pixel from ALandscape 52 | ALandscape* parent = (ALandscape*)GetAttachmentRootActor(); 53 | FVector out(PixelLoc.X, PixelLoc.Y, 0); 54 | 55 | FVector origin; 56 | FVector bounds; 57 | parent->GetActorBounds(true, origin, bounds); 58 | 59 | // Convert to normalized local space 60 | out /= FVector(NumPixelsX, NumPixelsY, 1.0); 61 | 62 | // Scale to Worldspace 63 | out *= 2.0 * bounds; 64 | 65 | // Offset by actor transform 66 | out += parent->GetActorLocation(); 67 | 68 | UE_LOG(LogTemp, Log, TEXT("World Space %s, pixel %s, norm %s, parent_loc: %s, origin %s, bounds: %s"), *out.ToString(), *PixelLoc.ToString(), *(PixelLoc / FVector2D(NumPixelsX, NumPixelsY)).ToString(), *parent->GetActorLocation().ToString(), *origin.ToString(), *bounds.ToString()); 69 | 70 | out.Z = parent->GetHeightAtLocation(out).Get(0); 71 | 72 | return out; 73 | } 74 | 75 | FVector2D UGISDataComponent::GetGPSLocation(const FVector& WorldSpaceCoordinate) 76 | { 77 | ALandscape* parent = (ALandscape*)GetAttachmentRootActor(); 78 | FVector origin; 79 | FVector bounds; 80 | 81 | parent->GetActorBounds(true, origin, bounds); 82 | 83 | // Get local position then normalize to pixel space 84 | FVector2D PixelCoordinate = FVector2D(parent->GetTransform().InverseTransformPosition(WorldSpaceCoordinate)); 85 | 86 | // Normalize local position 87 | PixelCoordinate /= FVector2D(bounds * 2.0f); 88 | 89 | // Scale to pixel space 90 | PixelCoordinate *= FVector2D(NumPixelsX, NumPixelsY); 91 | 92 | // Convert PixelSpace to projection 93 | FVector2D Projection = GDALHelpers::ApplyGeoTransform(GeoTransform.GetData(), PixelCoordinate); 94 | 95 | // Get Transform to lat long 96 | FString WGS84_WKT = GDALHelpers::WktFromEPSG(4326); 97 | OGRCoordinateTransformationRef CoordTransform = GDALHelpers::CreateCoordinateTransform(WKT, WGS84_WKT); 98 | 99 | FVector Out; 100 | check(GDALHelpers::TransformCoordinate(CoordTransform, FVector(Projection, 0), Out)) 101 | 102 | // Prior to GDAL 3.0, coordinate transformations expect WGS84 coordinates to be in (lon,lat) format instead of (lat,lon) 103 | // (See: https://gdal.org/tutorials/osr_api_tut.html#crs-and-axis-order) 104 | #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0) 105 | Out = FVector(Out.Y, Out.X, Out.Z); 106 | #endif 107 | 108 | return FVector2D(Out); 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LandscapeGen 2 | ============ 3 | 4 | This plugin allows users to generate landscapes from GIS data inside the Unreal Editor, leveraging the [GDAL/OGR](https://gdal.org/) API via the [UnrealGDAL](https://github.com/TensorWorks/UnrealGDAL) plugin. The following features are provided: 5 | 6 | - Supports generating landscapes in the Unreal Editor, which are then saved as regular assets that can be used in packaged projects. 7 | - Supports importing GIS data through both GDAL and [Mapbox](https://www.mapbox.com/). 8 | - Provides a [pluggable architecture](#plugin-architecture) so that developers can provide their own data source implementations. 9 | - Provides functionality to convert between geospatial coordinates and Unreal Engine worldspace coordinates. 10 | - Supports custom scale factors when generating landscapes. 11 | 12 | Please note the following limitations in the current implementation: 13 | 14 | - Landscapes can only be generated in the Unreal Editor, they cannot be generated at runtime. Generated landscapes are usable at runtime but still rely on the runtime modules from this plugin to provide coordinate conversion functionality. 15 | - Only one landscape can be generated at a time, and each generated landscape is independent. Automatically tiling multiple landscapes to create a seamless large-scale environment is not supported. 16 | - Importing GIS data through the GDAL data source is limited to the file formats supported by the UnrealGDAL plugin (which is effectively just GeoTIFF files in the current release.) 17 | 18 | 19 | ## Installation 20 | 21 | To use the LandscapeGen plugin in an Unreal Engine project, you will need to do the following: 22 | 23 | - Download the precompiled binaries for the [latest release of the UnrealGDAL plugin](https://github.com/TensorWorks/UnrealGDAL/releases) and place the `UnrealGDAL` folder inside your project's `Plugins` directory. 24 | - Download the source code for the LandscapeGen plugin and place the `LandscapeGen` folder inside your project's `Plugins` directory. 25 | - Ensure both plugins are enabled in your project's settings. 26 | 27 | Demo Editor Utility Widgets are provided in the [Examples content folder](./Content/Examples) and can be used to test the plugin's functionality. Widgets are provided for generating landscapes using both the GDAL and Mapbox data sources, and also for converting between geospatial coordinates and Unreal Engine worldspace coordinates. **Note that conversion results when using the example Widget will be inaccurate due to floating-point precision issues when using UMG text input elements, but the underlying functions will produce accurate results when accessed directly from C++ or Blueprints. The example Widget also only supports performing coordinate conversion for a single landscape and will not function correctly if there are multiple generated landscapes in a map.** 28 | 29 | 30 | ## Plugin architecture 31 | 32 | The plugin is composed of four modules: 33 | 34 | - [LandscapeGenRuntime](./Source/LandscapeGenRuntime): provides the functionality required at runtime to perform coordinate transformation. This consists of the [UGISDataComponent](./Source/LandscapeGenRuntime/Public/GISDataComponent.h) class, which is attached as a component of all generated landscape assets. 35 | 36 | - [LandscapeGenEditor](./Source/LandscapeGenEditor): provides the Editor-only functionality for generating landscapes, and defines the key classes and interfaces used by data source implementations. 37 | 38 | - [GDALDataSource](./Source/GDALDataSource): provides a data source implementation that uses the GDAL/OGR API to load GIS data from files on the local filesystem. 39 | 40 | - [MapboxDataSource](./Source/MapboxDataSource): provides a data source implementation that uses the Mapbox REST API to retrieve GIS data. 41 | 42 | The relationships between the core classes and interfaces of the plugin's modules are depicted below: 43 | 44 | ![Diagram depicting the relationships between the classes in the plugin](./Resources/architecture.svg) 45 | 46 | - [IGISDataSource](./Source/LandscapeGenEditor/Public/GISDataSource.h): this is the interface that needs to be implemented by all data sources providing input data to the landscape generation system. It consists of a single method called `RetrieveData()` which is expected to asynchronously perform data retrieval and then signal success or failure by invoking the appropriate callback. The callbacks for both success and failure have the same function signature, which takes an [FString](https://docs.unrealengine.com/en-US/API/Runtime/Core/Containers/FString/index.html) parameter indicating an error message and an [FGISData](./Source/LandscapeGenEditor/Public/GISData.h) parameter containing the retrieved data. In the event of a failure, the error message should be non-empty and the data object should be empty, and in the event of success the error message should be empty and the data object should be fully populated. The use of two separate callbacks with identical signatures (rather than a single callback) is for compatibility with asynchronous execution in Blueprints, which requires two separate callbacks to represent the two possible execution paths. (Asynchronous execution can be performed using the `RetrieveDataFromSource()` static method of the [UAsyncDataRetrieval](./Source/LandscapeGenEditor/Public/AsyncDataRetrieval.h) class, which is built atop the [UBlueprintAsyncActionBase](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Kismet/UBlueprintAsyncActionBase/index.html) base class that ships with the Unreal Engine.) 47 | 48 | - [FGISData](./Source/LandscapeGenEditor/Public/GISData.h): this object represents the GIS data that has been retrieved by a given data source and is used as the input data for the landscape generation system. The object contains buffers for both heightmap and RGB raster data, along with geospatial metadata such as the geospatial extents (corner coordinates) of the raster data and the [Well-Known Text (WKT)](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) representation of the projected coordinate system used by the raster data. **The landscape generation system requires that the raster data for both heightmap and RGB share the same geospatial extents and projected coordinate system.** 49 | 50 | - [ULandscapeGenerationBPFL](./Source/LandscapeGenEditor/Public/LandscapeGenerationBPFL.h): this class exposes the public functionality of the landscape generation system. The `GenerateLandscapeFromGISData()` static method is the function responsible for accepting GIS data (in the form of an [FGISData](./Source/LandscapeGenEditor/Public/GISData.h) object) and performing landscape generation. This function is accessible from both C++ and Blueprints. 51 | 52 | - [UGISDataComponent](./Source/LandscapeGenRuntime/Public/GISDataComponent.h): this class is attached as a component of all generated landscape assets and provides functionality to perform coordinate transformation. This functionality is accessible from both C++ and Blueprints. 53 | 54 | 55 | ## Legal 56 | 57 | Copyright © 2020, TensorWorks Pty Ltd. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details. 58 | -------------------------------------------------------------------------------- /Source/GDALDataSource/Private/GDALDataSource.cpp: -------------------------------------------------------------------------------- 1 | #include "GDALDataSource.h" 2 | #include "GDALHeaders.h" 3 | #include "GDALHelpers.h" 4 | #include "LandscapeConstraints.h" 5 | 6 | namespace 7 | { 8 | FString GetProjectionWkt(const GDALDatasetRef& dataset) 9 | { 10 | const char* projection = dataset->GetProjectionRef(); 11 | return ((projection != nullptr) ? FString(UTF8_TO_TCHAR(projection)) : FString() ); 12 | } 13 | } 14 | 15 | void UGDALDataSource::RetrieveData(FGISDataSourceDelegate OnSuccess, FGISDataSourceDelegate OnFailure) 16 | { 17 | // Attempt to perform data retrieval 18 | FGISData data; 19 | FString error = this->RetrieveDataInternal(data); 20 | 21 | //Notify the appropriate delegate of our success or failure 22 | if (error.IsEmpty()) { 23 | OnSuccess.Broadcast(error, data); 24 | } 25 | else { 26 | OnFailure.Broadcast(error, data); 27 | } 28 | } 29 | 30 | FString UGDALDataSource::RetrieveDataInternal(FGISData& data) 31 | { 32 | //------- STEP 1: OPEN DATASETS ------- 33 | 34 | // Attempt to open the heightmap dataset 35 | GDALDatasetRef heightmap = mergetiff::DatasetManagement::openDataset(TCHAR_TO_UTF8(*(this->HeightmapDataset))); 36 | if (!heightmap) { 37 | return TEXT("Failed to open the heightmap dataset"); 38 | } 39 | 40 | // Attempt to open the RGB dataset 41 | GDALDatasetRef rgb = mergetiff::DatasetManagement::openDataset(TCHAR_TO_UTF8(*(this->RGBDataset))); 42 | if (!rgb) { 43 | return TEXT("Failed to open the RGB dataset"); 44 | } 45 | 46 | 47 | //------- STEP 2: RETRIEVE DATASET METADATA ------- 48 | 49 | // Attempt to retrieve the Well-Known Text (WKT) representation of the projected coordinate system used by the heightmap dataset 50 | FString heightmapWkt = GetProjectionWkt(heightmap); 51 | if (heightmapWkt.IsEmpty()) { 52 | return TEXT("Failed to retrieve the projected coordinate system used by the heightmap dataset"); 53 | } 54 | 55 | // Attempt to retrieve the Well-Known Text (WKT) representation of the projected coordinate system used by the RGB dataset 56 | FString rgbWkt = GetProjectionWkt(rgb); 57 | if (rgbWkt.IsEmpty()) { 58 | return TEXT("Failed to retrieve the projected coordinate system used by the RGB dataset"); 59 | } 60 | 61 | // Attempt to retrieve the projected corner coordinates for the heightmap dataset 62 | RasterCornerCoordinatesRef heightmapCorners = GDALHelpers::GetRasterCorners(heightmap); 63 | if (!heightmapCorners) { 64 | return TEXT("Failed to compute the projected corner coordinates of the heightmap dataset"); 65 | } 66 | 67 | // Attempt to retrieve the projected corner coordinates for the RGB dataset 68 | RasterCornerCoordinatesRef rgbCorners = GDALHelpers::GetRasterCorners(rgb); 69 | if (!rgbCorners) { 70 | return TEXT("Failed to compute the projected corner coordinates of the RGB dataset"); 71 | } 72 | 73 | 74 | //------- STEP 3: VERIFY METADATA VALIDITY ------- 75 | 76 | // Verify that the two datasets use the same projected coordinate system 77 | if (heightmapWkt.Equals(rgbWkt) == false) { 78 | return TEXT("The heightmap dataset and RGB dataset must use the same projected coordinate system"); 79 | } 80 | 81 | // Verify that the two datasets have the same corner coordinates 82 | if (heightmapCorners->UpperLeft != rgbCorners->UpperLeft || heightmapCorners->LowerRight != rgbCorners->LowerRight) { 83 | return TEXT("The heightmap dataset and RGB dataset must have the same geographic extents"); 84 | } 85 | 86 | // Verify that the RGB dataset has at least 3 bands 87 | if (rgb->GetRasterCount() < 3) { 88 | return TEXT("RGB dataset must contain R, G and B channels"); 89 | } 90 | 91 | // Verify that the heightmap dataset does not exceed the maximum supported raster size for landscape generation 92 | if (heightmap->GetRasterXSize() > LandscapeConstraints::MaxRasterSizeX() || heightmap->GetRasterYSize() > LandscapeConstraints::MaxRasterSizeY()) 93 | { 94 | return FString::Printf( 95 | TEXT("Heightmap raster size of %llux%llu exceeds maximum supported size of %llux%llu"), 96 | heightmap->GetRasterXSize(), 97 | heightmap->GetRasterYSize(), 98 | LandscapeConstraints::MaxRasterSizeX(), 99 | LandscapeConstraints::MaxRasterSizeY() 100 | ); 101 | } 102 | 103 | // Verify that the RGB dataset does not exceed the maximum supported raster size for landscape generation 104 | if (rgb->GetRasterXSize() > LandscapeConstraints::MaxRasterSizeX() || rgb->GetRasterYSize() > LandscapeConstraints::MaxRasterSizeY()) 105 | { 106 | return FString::Printf( 107 | TEXT("RGB raster size of %llux%llu exceeds maximum supported size of %llux%llu"), 108 | rgb->GetRasterXSize(), 109 | rgb->GetRasterYSize(), 110 | LandscapeConstraints::MaxRasterSizeX(), 111 | LandscapeConstraints::MaxRasterSizeY() 112 | ); 113 | } 114 | 115 | 116 | //------- STEP 4: EMIT WARNINGS FOR KNOWN PROBLEMATIC METADATA ------- 117 | 118 | // Emit a warning if the heightmap data contains nodata values 119 | int hasNoDataValue = 0; 120 | double noDataValue = heightmap->GetRasterBand(1)->GetNoDataValue(&hasNoDataValue); 121 | if (hasNoDataValue) { 122 | UE_LOG(LogTemp, Warning, TEXT("Heightmap dataset contains nodata values, this can interfere with height scaling!")); 123 | } 124 | 125 | // Emit a warning if the colour interpretation metadata for the raster bands do not indicate an RGB image 126 | GDALColorInterp expectedInterp[] = { GCI_RedBand, GCI_GreenBand, GCI_BlueBand }; 127 | for (int index = 1; index <= 3; ++index) 128 | { 129 | if (rgb->GetRasterBand(index)->GetColorInterpretation() != expectedInterp[index]) 130 | { 131 | UE_LOG(LogTemp, Warning, TEXT("RGB dataset metadata does not indicate R,G,B ordering for raster bands!")); 132 | break; 133 | } 134 | } 135 | 136 | 137 | //------- STEP 5: RASTER DATA PREPROCESSING ------- 138 | 139 | // If the heightmap data is not already in Float32 format then convert it 140 | if (heightmap->GetRasterBand(1)->GetRasterDataType() != GDT_Float32) 141 | { 142 | // Attempt to convert the heightmap data to Float32 143 | heightmap = GDALHelpers::Translate(heightmap, GDALHelpers::UniqueMemFilename(), 144 | GDALHelpers::ParseGDALTranslateOptions({ 145 | TEXT("-ot"), 146 | TEXT("Float32"), 147 | }) 148 | ); 149 | 150 | // Verify that conversion succeeded 151 | if (!heightmap) { 152 | return TEXT("Failed to convert the heightmap to Float32 format"); 153 | } 154 | } 155 | 156 | 157 | //------- STEP 6: READ HEIGHTMAP RASTER DATA ------- 158 | 159 | // Retrieve the raster dimensions for the heightmap 160 | data.HeightBufferX = heightmap->GetRasterXSize(); 161 | data.HeightBufferY = heightmap->GetRasterYSize(); 162 | 163 | // Create a buffer to hold the heightmap data and wrap it in a RasterData object 164 | mergetiff::RasterData heightmapData = GDALHelpers::AllocateAndWrap(data.HeightBuffer, 1, data.HeightBufferY, data.HeightBufferX, 0.0f); 165 | 166 | // Attempt to read the heightmap data into our buffer 167 | if (mergetiff::RasterIO::readDataset(heightmap, heightmapData, {1}) == false) { 168 | return TEXT("Failed to read the data from the heightmap"); 169 | } 170 | 171 | 172 | //------- STEP 7: READ RGB RASTER DATA ------- 173 | 174 | // Retrieve the raster dimensions for the RGB data 175 | data.ColorBufferX = rgb->GetRasterXSize(); 176 | data.ColorBufferY = rgb->GetRasterYSize(); 177 | 178 | // Create a buffer to hold the RGBA data and wrap it in a RasterData object, filling all channels with 255 by default 179 | data.PixelFormat = EPixelFormat::PF_R8G8B8A8; 180 | mergetiff::RasterData rgbaData = GDALHelpers::AllocateAndWrap(data.ColorBuffer, 4, data.ColorBufferY, data.ColorBufferX, 255); 181 | 182 | // Attempt to read the RGB data into our buffer, leaving the alpha channel filled with 255 183 | if (mergetiff::RasterIO::readDataset(rgb, rgbaData, {1,2,3}) == false) { 184 | return TEXT("Failed to read the data from the RGB dataset"); 185 | } 186 | 187 | 188 | //------- STEP 8: STORE REQUIRED METADATA ------- 189 | 190 | // Store the corner coordinates 191 | data.CornerType = ECornerCoordinateType::Projected; 192 | data.UpperLeft = heightmapCorners->UpperLeft; 193 | data.LowerRight = heightmapCorners->LowerRight; 194 | 195 | // Store the projected coordinate system WKT 196 | data.ProjectionWKT = heightmapWkt; 197 | 198 | // If we reach this point then data retrieval succeeded 199 | return TEXT(""); 200 | } 201 | -------------------------------------------------------------------------------- /Source/LandscapeGenEditor/Private/LandscapeGenerationBPFL.cpp: -------------------------------------------------------------------------------- 1 | #include "LandscapeGenerationBPFL.h" 2 | #include "Landscape.h" 3 | #include "LandscapeEdit.h" 4 | 5 | #include "GDALHelpers.h" 6 | #include "GISDataComponent.h" 7 | #include "LandscapeConstraints.h" 8 | 9 | #include "AssetRegistryModule.h" 10 | #include "AssetToolsModule.h" 11 | 12 | #include "Factories/TextureFactory.h" 13 | 14 | #include "Factories/MaterialFactoryNew.h" 15 | #include "Materials/MaterialInstanceDynamic.h" 16 | #include "Materials/MaterialExpressionLandscapeLayerCoords.h" 17 | #include "Materials/MaterialExpressionDivide.h" 18 | #include "Materials/MaterialExpressionMultiply.h" 19 | #include "Materials/MaterialExpressionAppendVector.h" 20 | #include "Materials/MaterialExpressionScalarParameter.h" 21 | #include "Materials/MaterialExpressionTextureSampleParameter2D.h" 22 | 23 | #include 24 | 25 | ALandscape* ULandscapeGenerationBPFL::GenerateLandscapeFromGISData( 26 | const UObject* WorldContext, const FString& LandscapeName, const FGISData& GISData, const FVector& Scale3D) 27 | { 28 | // Check that we can allocate GISData in one texture 29 | if ( 30 | GISData.HeightBufferX > LandscapeConstraints::MaxRasterSizeX() || 31 | GISData.HeightBufferY > LandscapeConstraints::MaxRasterSizeY() || 32 | GISData.ColorBufferX > LandscapeConstraints::MaxRasterSizeX() || 33 | GISData.ColorBufferY > LandscapeConstraints::MaxRasterSizeY() 34 | ) { 35 | UE_LOG(LogTemp, Log, TEXT("Textures too large to allocate in single landscape")); 36 | return nullptr; 37 | } 38 | 39 | auto ColorBuffer = GISData.ColorBuffer; 40 | 41 | // Textures need to be in BGRA format, so reorder the raster channels if the input data is in another format 42 | if (GISData.PixelFormat != EPixelFormat::PF_B8G8R8A8) 43 | { 44 | // Determine the correct mapping from source channel order to destination channel order 45 | // (Note that these are 1-indexed to remain consistent with the way GDAL 1-indexes raster bands) 46 | std::vector ChannelMapping; 47 | switch (GISData.PixelFormat) 48 | { 49 | case EPixelFormat::PF_R8G8B8A8: 50 | ChannelMapping = {3,2,1,4}; 51 | break; 52 | default: 53 | UE_LOG(LogTemp, Log, TEXT("Unsupported pixel format for colour data")); 54 | return nullptr; 55 | } 56 | 57 | mergetiff::RasterData dataWrapper(ColorBuffer.GetData(), 4, GISData.ColorBufferY, GISData.ColorBufferX, true); 58 | GDALDatasetRef datasetWrapper = mergetiff::DatasetManagement::wrapRasterData(dataWrapper); 59 | TArray remapped; 60 | mergetiff::RasterData remappedRD = GDALHelpers::AllocateAndWrap(remapped, 4, GISData.ColorBufferY, GISData.ColorBufferX, 255); 61 | 62 | if (mergetiff::RasterIO::readDataset(datasetWrapper, remappedRD, ChannelMapping) == false) 63 | { 64 | UE_LOG(LogTemp, Log, TEXT("Failed to remap channels for the colour data")); 65 | return nullptr; 66 | } 67 | 68 | ColorBuffer = remapped; 69 | } 70 | 71 | // Save colour texture to UAsset 72 | FString PackageName = TEXT("/Game/GISLandscapeData/"); 73 | 74 | // Get unique asset name 75 | FString Name; 76 | FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); 77 | AssetToolsModule.Get().CreateUniqueAssetName(PackageName, FString::Printf(TEXT("T_%s_GISTexture"), *LandscapeName), PackageName, Name); 78 | 79 | // Create package 80 | UPackage* Package = CreatePackage(NULL, *PackageName); 81 | Package->FullyLoad(); 82 | 83 | UTexture2D* ColorTexture; 84 | 85 | UTextureFactory* TextureFactory = NewObject(); 86 | TextureFactory->AddToRoot(); 87 | 88 | ColorTexture = (UTexture2D*)TextureFactory->CreateTexture2D( 89 | Package, *Name, RF_Public | RF_Standalone | RF_Transactional); 90 | 91 | if (ColorTexture) 92 | { 93 | ColorTexture->Source.Init( 94 | GISData.ColorBufferX, 95 | GISData.ColorBufferY, 96 | /*NumSlices=*/ 1, 97 | /*NumMips=*/ 1, 98 | ETextureSourceFormat::TSF_BGRA8, 99 | ColorBuffer.GetData() 100 | ); 101 | ColorTexture->CompressionSettings = TC_Default; 102 | ColorTexture->LODGroup = TEXTUREGROUP_World; 103 | ColorTexture->MipGenSettings = TMGS_NoMipmaps; 104 | } 105 | 106 | GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(TextureFactory, ColorTexture); 107 | 108 | ColorTexture->PostEditChange(); 109 | TextureFactory->RemoveFromRoot(); 110 | 111 | FAssetRegistryModule::AssetCreated(ColorTexture); 112 | Package->SetDirtyFlag(true); 113 | 114 | // Setup heightmap for landscape 115 | TMap> HeightmapDataPerLayers; 116 | TMap> MaterialLayerDataPerLayer; 117 | 118 | mergetiff::RasterData floatRasterData(const_cast(GISData.HeightBuffer.GetData()), 1, GISData.HeightBufferY, GISData.HeightBufferX, true); 119 | GDALDatasetRef floatDataset = mergetiff::DatasetManagement::datasetFromRaster(floatRasterData); 120 | 121 | // Get scale min and maxes in meters as float 122 | auto MinMax = GDALHelpers::ComputeRasterMinMax(floatDataset, 1); 123 | 124 | check(MinMax) 125 | 126 | // Convert meters in float to uint16 for Unreal while maximizing height sample resolution 127 | GDALDatasetRef uint16Dataset = GDALHelpers::Translate(floatDataset, GDALHelpers::UniqueMemFilename(), 128 | GDALHelpers::ParseGDALTranslateOptions({ 129 | TEXT("-ot"), 130 | TEXT("UInt16"), 131 | TEXT("-scale"), 132 | FString::Printf(TEXT("%lf"), MinMax->Min), 133 | FString::Printf(TEXT("%lf"), MinMax->Max), 134 | FString::Printf(TEXT("%d"), 0), 135 | FString::Printf(TEXT("%d"), MAX_uint16) 136 | }) 137 | ); 138 | 139 | TArray HeightGDAL; 140 | 141 | // Create a buffer to hold the heightmap data and wrap it in a RasterData object 142 | mergetiff::RasterData heightmapData = GDALHelpers::AllocateAndWrap(HeightGDAL, 1, GISData.HeightBufferY, GISData.HeightBufferX, 0); 143 | 144 | // Attempt to read the heightmap data into our buffer 145 | if (mergetiff::RasterIO::readDataset(uint16Dataset, heightmapData, { 1 }) == false) { 146 | UE_LOG(LogTemp, Log, TEXT("Failed to read the data from the heightmap")); 147 | return nullptr; 148 | } 149 | 150 | // Store corner coordinates projected if not already 151 | FVector2D UpperLeft = GISData.UpperLeft; 152 | FVector2D LowerRight = GISData.LowerRight; 153 | 154 | if (GISData.CornerType == ECornerCoordinateType::LatLon) 155 | { 156 | // Prior to GDAL 3.0, coordinate transformations expect WGS84 coordinates to be in (lon,lat) format instead of (lat,lon) 157 | // (See: https://gdal.org/tutorials/osr_api_tut.html#crs-and-axis-order) 158 | #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3,0,0) 159 | UpperLeft = FVector2D(UpperLeft.Y, UpperLeft.X); 160 | LowerRight = FVector2D(LowerRight.Y, LowerRight.X); 161 | #endif 162 | 163 | FString WGS84_WKT = GDALHelpers::WktFromEPSG(4326); 164 | OGRCoordinateTransformationRef CoordTransform = GDALHelpers::CreateCoordinateTransform(WGS84_WKT, GISData.ProjectionWKT); 165 | 166 | FVector TempUL; 167 | check(GDALHelpers::TransformCoordinate(CoordTransform, FVector(UpperLeft, 0), TempUL)); 168 | UpperLeft = FVector2D(TempUL); 169 | 170 | FVector TempLR; 171 | check(GDALHelpers::TransformCoordinate(CoordTransform, FVector(LowerRight, 0), TempLR)); 172 | LowerRight = FVector2D(TempLR); 173 | } 174 | 175 | check(GDALHelpers::SetRasterCorners(uint16Dataset, UpperLeft, LowerRight)) 176 | 177 | // Make the scale factor for X Y by calculating metres per pixel 178 | // Make Z scale factor as Unreals default heighmap range is -255cm to 255cm over a 0 to max_uint16 range 179 | FVector ScaleVector = Scale3D * 100 * FVector((LowerRight - UpperLeft).GetAbs() / FVector2D(GISData.HeightBufferX, GISData.HeightBufferY), (MinMax->Max - MinMax->Min) / 512.0); 180 | 181 | ALandscape* Landscape = WorldContext->GetWorld()->SpawnActor(); 182 | 183 | // Setup landscape configuration 184 | Landscape->ComponentSizeQuads = 255; 185 | Landscape->SubsectionSizeQuads = 255; 186 | Landscape->NumSubsections = 1; 187 | Landscape->SetLandscapeGuid(FGuid::NewGuid()); 188 | 189 | // Build new material for landscape 190 | Landscape->LandscapeMaterial = GenerateUnlitLandscapeMaterial(LandscapeName, FString::Printf(TEXT("%s.%s"), *PackageName, *Name), FMath::CeilToInt(GISData.HeightBufferX / 255), FMath::CeilToInt(GISData.HeightBufferY / 255), 255); 191 | 192 | Landscape->CreateLandscapeInfo(); 193 | Landscape->SetActorTransform(FTransform(FQuat::Identity, FVector(), ScaleVector)); 194 | 195 | // Generate LandscapeActor from heightmap 196 | HeightmapDataPerLayers.Add(FGuid(), HeightGDAL); 197 | MaterialLayerDataPerLayer.Add(FGuid(), TArray()); 198 | 199 | // Build in engine only function for taking height buffer and generating landscape components 200 | Landscape->Import(Landscape->GetLandscapeGuid(), 0, 0, GISData.HeightBufferX - 1, 201 | GISData.HeightBufferY - 1, Landscape->NumSubsections, Landscape->SubsectionSizeQuads, HeightmapDataPerLayers, 202 | TEXT("NONE"), MaterialLayerDataPerLayer, ELandscapeImportAlphamapType::Layered 203 | ); 204 | 205 | // Translate Landscape so that lowest point is 0 in WorldSpace 206 | FVector LandscapeOrigin; 207 | FVector LandscapeBounds; 208 | Landscape->GetActorBounds(false, LandscapeOrigin, LandscapeBounds); 209 | Landscape->SetActorLocation(FVector(0, 0, LandscapeBounds.Z)); 210 | 211 | // Create attach and register GISDataComponent 212 | UGISDataComponent* GISDataComponent = NewObject(Landscape, NAME_None, RF_Transactional); 213 | GISDataComponent = (UGISDataComponent*)Landscape->CreateComponentFromTemplate(GISDataComponent, NAME_None); 214 | GISDataComponent->SetupAttachment(Landscape->GetRootComponent(), NAME_None); 215 | Landscape->ReregisterAllComponents(); 216 | 217 | // Get last components to fill out useful information 218 | ULandscapeComponent* lastComponent = Landscape->LandscapeComponents.Last(); 219 | GISDataComponent->NumComponentsX = lastComponent->SectionBaseX / lastComponent->ComponentSizeQuads + 1; 220 | GISDataComponent->NumComponentsY = lastComponent->SectionBaseY / lastComponent->ComponentSizeQuads + 1; 221 | GISDataComponent->ComponentSizeQuads = lastComponent->ComponentSizeQuads; 222 | 223 | // Fill GIS data into the component 224 | GISDataComponent->UpperLeft = UpperLeft; 225 | GISDataComponent->LowerRight = LowerRight; 226 | GISDataComponent->SetGeoTransforms(uint16Dataset); 227 | GISDataComponent->WKT = GISData.ProjectionWKT; 228 | GISDataComponent->NumPixelsX = GISData.HeightBufferX; 229 | GISDataComponent->NumPixelsY = GISData.HeightBufferY; 230 | 231 | Landscape->CreateLandscapeInfo(); 232 | Landscape->SetActorLabel(LandscapeName); 233 | 234 | return Landscape; 235 | } 236 | 237 | UMaterial* ULandscapeGenerationBPFL::GenerateUnlitLandscapeMaterial(const FString& LandscapeName, 238 | const FString& TexturePath, const int32& NumComponentsX, const int32& NumComponentsY, const int32& NumQuads) 239 | { 240 | FString PackageName = "/Game/GISLandscapeData/"; 241 | 242 | // Get unique name 243 | FString Name; 244 | FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); 245 | AssetToolsModule.Get().CreateUniqueAssetName(PackageName, FString::Printf(TEXT("M_%s_Unlit"), *LandscapeName), PackageName, Name); 246 | 247 | UMaterial* UnrealMaterial; 248 | 249 | // Create package 250 | UPackage* Package = CreatePackage(NULL, *PackageName); 251 | Package->FullyLoad(); 252 | 253 | // Create an unreal material asset 254 | auto MaterialFactory = NewObject(); 255 | UnrealMaterial = (UMaterial*)MaterialFactory->FactoryCreateNew(UMaterial::StaticClass(), Package, *Name, RF_Standalone | RF_Public, NULL, GWarn); 256 | 257 | UnrealMaterial->SetShadingModel(EMaterialShadingModel::MSM_Unlit); 258 | 259 | auto NumCompsX = NewObject(UnrealMaterial); 260 | NumCompsX->DefaultValue = NumComponentsX; 261 | NumCompsX->ParameterName = FName("NumComponentsX"); 262 | 263 | auto NumCompsY = NewObject(UnrealMaterial); 264 | NumCompsY->DefaultValue = NumComponentsY; 265 | NumCompsY->ParameterName = FName("NumComponentsY"); 266 | 267 | auto Append = NewObject(UnrealMaterial); 268 | Append->A.Connect(0, NumCompsX); 269 | Append->B.Connect(0, NumCompsY); 270 | 271 | auto NumQuadsParam = NewObject(UnrealMaterial); 272 | NumQuadsParam->DefaultValue = NumQuads; 273 | NumQuadsParam->ParameterName = FName("NumQuads"); 274 | 275 | auto Multiply = NewObject(UnrealMaterial); 276 | Multiply->A.Connect(0, Append); 277 | Multiply->B.Connect(0, NumQuadsParam); 278 | 279 | auto LandscapeCoords = NewObject(UnrealMaterial); 280 | 281 | auto Divide = NewObject(UnrealMaterial); 282 | Divide->A.Connect(0, LandscapeCoords); 283 | Divide->B.Connect(0, Multiply); 284 | 285 | // Make texture sampler 286 | auto Texture = NewObject(UnrealMaterial); 287 | Texture->Texture = LoadObject(nullptr, *FString::Printf(TEXT("Texture2D'%s'"), *TexturePath)); 288 | Texture->ParameterName = FName("ColorMap"); 289 | Texture->SamplerType = SAMPLERTYPE_Color; 290 | Texture->Coordinates.Connect(0, Divide); 291 | 292 | UnrealMaterial->Expressions.Add(Texture); 293 | UnrealMaterial->EmissiveColor.Expression = Texture; 294 | 295 | // let the material update itself if necessary 296 | UnrealMaterial->PreEditChange(NULL); 297 | UnrealMaterial->PostEditChange(); 298 | 299 | UnrealMaterial->UpdateCachedExpressionData(); 300 | 301 | FAssetRegistryModule::AssetCreated(UnrealMaterial); 302 | Package->SetDirtyFlag(true); 303 | 304 | return UnrealMaterial; 305 | } 306 | -------------------------------------------------------------------------------- /Source/MapboxDataSource/Private/MapboxDataSource.cpp: -------------------------------------------------------------------------------- 1 | #include "MapboxDataSource.h" 2 | #include "Modules/ModuleManager.h" 3 | #include "IImageWrapper.h" 4 | #include "IImageWrapperModule.h" 5 | #include "Interfaces/IHttpResponse.h" 6 | #include "HttpModule.h" 7 | #include "ImageUtils.h" 8 | #include "GDALHelpers.h" 9 | #include "LandscapeConstraints.h" 10 | 11 | namespace 12 | { 13 | float long2tilexf(double lon, int z) 14 | { 15 | return (lon + 180.0) / 360.0 * (1 << z); 16 | } 17 | 18 | float lat2tileyf(double lat, int z) 19 | { 20 | double latrad = lat * PI/180.0; 21 | return (1.0 - asinh(tan(latrad)) / PI) / 2.0 * (1 << z); 22 | } 23 | 24 | int long2tilex(double lon, int z) 25 | { 26 | return (int)(floor(long2tilexf(lon, z))); 27 | } 28 | 29 | int lat2tiley(double lat, int z) 30 | { 31 | return (int)(floor(lat2tileyf(lat, z))); 32 | } 33 | 34 | double tilex2long(int x, int z) 35 | { 36 | return x / (double)(1 << z) * 360.0 - 180; 37 | } 38 | 39 | double tiley2lat(int y, int z) 40 | { 41 | double n = PI - 2.0 * PI * y / (double)(1 << z); 42 | return 180.0 / PI * atan(0.5 * (exp(n) - exp(-n))); 43 | } 44 | } 45 | 46 | const FString UMapboxDataSource::ProjectionWKT = GDALHelpers::WktFromEPSG(3857); 47 | 48 | void UMapboxDataSource::RetrieveData(FGISDataSourceDelegate InOnSuccess, FGISDataSourceDelegate InOnFailure) 49 | { 50 | this->OnSuccess = InOnSuccess; 51 | this->OnFailure = InOnFailure; 52 | this->RequestSectionRGBHeight(this->reqUpperLat, this->reqLeftLon, this->reqLowerLat, this->reqRightLon, this->reqZoom); 53 | } 54 | 55 | bool UMapboxDataSource::ValidateRequest(FString& OutError) 56 | { 57 | const int minZoom = 0; 58 | const int maxZoom = 15; 59 | 60 | const float minLat = -85.0511f; 61 | const float maxLat = 85.0511f; 62 | const float minLon = -180.0f; 63 | const float maxLon = 180.0f; 64 | 65 | if (this->reqZoom < minZoom || this->reqZoom > maxZoom) 66 | { 67 | OutError = FString::Printf(TEXT("Zoom out of valid Range (%d-%d)"), minZoom, maxZoom); 68 | return false; 69 | } 70 | 71 | if (this->reqUpperLat < this->reqLowerLat) 72 | { 73 | OutError = FString(TEXT("Upper Latitude value is lower than Lower Latitude value")); 74 | return false; 75 | } 76 | 77 | if (this->reqRightLon < this->reqLeftLon) 78 | { 79 | OutError = FString(TEXT("Right Longitude value is lower than Left Longitude value")); 80 | return false; 81 | } 82 | 83 | if (this->reqUpperLat > maxLat || this->reqLowerLat < minLat) 84 | { 85 | OutError = FString::Printf(TEXT("Upper or Lower Latitude value out of range (%f-%f)"), minLat, maxLat); 86 | return false; 87 | } 88 | 89 | if (this->reqRightLon > maxLon || this->reqLeftLon < minLon) 90 | { 91 | OutError = FString::Printf(TEXT("Upper or Lower Latitude value out of range (%f-%f)"), minLon, maxLon); 92 | return false; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | void UMapboxDataSource::RequestSectionRGBHeight(float upperLat, float leftLon, float lowerLat, float rightLon, int zoom) 99 | { 100 | this->hasReqFailed = false; 101 | 102 | // Check that request values are valid 103 | FString ValidationError; 104 | if (!this->ValidateRequest(ValidationError)) 105 | { 106 | this->OnFailure.Broadcast(ValidationError, FGISData()); 107 | return; 108 | } 109 | 110 | const int dimx = 256; 111 | const int dimy = 256; 112 | 113 | // Find tile indice bounds for given coordinates and zoom 114 | float minxf = long2tilexf(leftLon, zoom); 115 | float minyf = lat2tileyf(upperLat, zoom); 116 | 117 | float maxxf = long2tilexf(rightLon, zoom); 118 | float maxyf = lat2tileyf(lowerLat, zoom); 119 | 120 | ////////// Make requests produce square results for now ///////////// 121 | 122 | minxf = (int)(floor(minxf)); 123 | minyf = (int)(floor(minyf)); 124 | 125 | maxxf = (int)(floor(maxxf)); 126 | maxyf = (int)(floor(maxyf)); 127 | 128 | // Expand either x or y dimension to produce square result 129 | bool isXLarger = (maxxf - minxf) >= (maxyf - minyf); 130 | float largerSize = isXLarger ? (maxxf - minxf) : (maxyf - minyf); 131 | float& smallerMin = isXLarger ? minyf : minxf; 132 | float& smallerMax = isXLarger ? maxyf : maxxf; 133 | 134 | while (largerSize > (smallerMax - smallerMin)) 135 | { 136 | if (((int)(smallerMax - smallerMin)) % 2) { 137 | smallerMin--; 138 | } else { 139 | smallerMax++; 140 | } 141 | } 142 | 143 | ////////// Make requests produce square results for now ///////////// 144 | 145 | int minx = (int)(floor(minxf)); 146 | int miny = (int)(floor(minyf)); 147 | 148 | int maxx = (int)(floor(maxxf)); 149 | int maxy = (int)(floor(maxyf)); 150 | 151 | // /v4/{tileset_id}/{zoom}/{x}/{y}{@2x}.{format} 152 | // https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=YOUR_MAPBOX_ACCESS_TOKEN 153 | // https://api.mapbox.com/v4/mapbox.satellite/1/0/0@2x.jpg90?access_token=YOUR_MAPBOX_ACCESS_TOKEN 154 | // Set up parts of RGB and Height requests 155 | FString BaseURL = TEXT("https://api.mapbox.com/v4/"); 156 | 157 | FString TextureTilesetId = TEXT("mapbox.satellite"); 158 | FString TextureFormat = TEXT(".jpg90"); 159 | 160 | FString HeightTilesetId = TEXT("mapbox.terrain-rgb"); 161 | FString HeightFormat = TEXT(".pngraw"); 162 | 163 | FString ApiKey = this->reqAPIKey; 164 | 165 | // Set some useful variables for use by individual requests 166 | UMapboxDataSource* RequestTask = this; 167 | RequestTask->OffsetX = minx; 168 | RequestTask->OffsetY = miny; 169 | RequestTask->MaxX = maxx - minx + 1; 170 | RequestTask->MaxY = maxy - miny + 1; 171 | RequestTask->DimX = dimx * RequestTask->MaxX; 172 | RequestTask->DimY = dimy * RequestTask->MaxY; 173 | RequestTask->TileDimX = dimx; 174 | RequestTask->TileDimY = dimy; 175 | RequestTask->Zoom = zoom; 176 | 177 | // For properly cropped results: 178 | // RequestTask->MinU = (minxf - ((float)minx)) / ((float)RequestTask->MaxX); 179 | // RequestTask->MaxU = (maxxf - ((float)maxx)) / ((float)RequestTask->MaxX); 180 | // RequestTask->MinV = (minyf - ((float)miny)) / ((float)RequestTask->MaxY); 181 | // RequestTask->MaxV = (maxyf - ((float)maxy)) / ((float)RequestTask->MaxY); 182 | // (We are currently producing square results) 183 | 184 | // Used for cropping, we aren't cropping the results at the moment so just set UV bounds to (0-1) 185 | RequestTask->MinU = 0.0f; 186 | RequestTask->MaxU = 1.0f; 187 | RequestTask->MinV = 0.0f; 188 | RequestTask->MaxV = 1.0f; 189 | 190 | // RequestTask->NumXHeightPixels = abs(maxxf - minxf) * dimx; 191 | // RequestTask->NumYHeightPixels = abs(maxyf - minyf) * dimy; 192 | 193 | // Set size of output arrays for RGB and height data 194 | RequestTask->NumXHeightPixels = RequestTask->DimX; 195 | RequestTask->NumYHeightPixels = RequestTask->DimY; 196 | 197 | // check that number of tiles is smaller than max texture size 198 | if (RequestTask->NumXHeightPixels > LandscapeConstraints::MaxRasterSizeX() || RequestTask->NumYHeightPixels > LandscapeConstraints::MaxRasterSizeY()) 199 | { 200 | FString ErrString = FString::Printf(TEXT("%d x %d tile dims too large please reduce to 64 x 64 by lowering zoom level or reducing area"), (maxx - minx), (maxy - miny)); 201 | this->OnFailure.Broadcast(ErrString, FGISData()); 202 | return; 203 | } 204 | 205 | RequestTask->RGBData = TArray(); 206 | RequestTask->RGBData.AddDefaulted(RequestTask->NumXHeightPixels * RequestTask->NumYHeightPixels); 207 | 208 | RequestTask->HeightData = TArray(); 209 | RequestTask->HeightData.AddDefaulted(RequestTask->NumXHeightPixels * RequestTask->NumYHeightPixels); 210 | 211 | // Iterate over range of tile indices and create RGB and height data requests for each 212 | for (int x = minx; x <= maxx; x++) 213 | { 214 | for (int y = miny; y <= maxy; y++) 215 | { 216 | FString TextureRequestURL = FString::Printf(TEXT("%s%s/%d/%d/%d%s?access_token=%s"), *BaseURL, *TextureTilesetId, zoom, x, y, *TextureFormat, *ApiKey); 217 | FMapboxRequestData TextureReqData = { EMapboxRequestDataType::RGB, x, y }; 218 | RequestTask->Start(TextureRequestURL, TextureReqData); 219 | 220 | FString HeightRequestURL = FString::Printf(TEXT("%s%s/%d/%d/%d%s?access_token=%s"), *BaseURL, *HeightTilesetId, zoom, x, y, *HeightFormat, *ApiKey); 221 | FMapboxRequestData HeightReqData = { EMapboxRequestDataType::HEIGHT, x, y }; 222 | RequestTask->Start(HeightRequestURL, HeightReqData); 223 | } 224 | } 225 | } 226 | 227 | void UMapboxDataSource::Start(FString URL, FMapboxRequestData data) 228 | { 229 | // Create the Http request and add to pending request list 230 | TSharedRef HttpRequest = FHttpModule::Get().CreateRequest(); 231 | 232 | UE_LOG(LogTemp, Log, TEXT("Sent Request: %s"), *URL); 233 | 234 | HttpRequest->OnProcessRequestComplete().BindUObject(this, &UMapboxDataSource::HandleMapboxRequest, data); 235 | HttpRequest->SetURL(URL); 236 | HttpRequest->SetVerb(TEXT("GET")); 237 | HttpRequest->ProcessRequest(); 238 | 239 | this->TotalRequests++; 240 | } 241 | 242 | void UMapboxDataSource::HandleMapboxRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded, FMapboxRequestData data) 243 | { 244 | RemoveFromRoot(); 245 | 246 | // Check if the HTTP requests succeeded, enter failure state if any request fails 247 | if ( bSucceeded && HttpResponse.IsValid() && HttpResponse->GetContentLength() > 0 ) 248 | { 249 | IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); 250 | TSharedPtr ImageWrappers[3] = 251 | { 252 | ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG), 253 | ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG), 254 | ImageWrapperModule.CreateImageWrapper(EImageFormat::BMP), 255 | }; 256 | 257 | // Iterate of image wrappers until one is valid for the received data 258 | for ( auto ImageWrapper : ImageWrappers ) 259 | { 260 | if ( ImageWrapper.IsValid() && ImageWrapper->SetCompressed(HttpResponse->GetContent().GetData(), HttpResponse->GetContentLength()) ) 261 | { 262 | // Determine which tile index we are dealing with 263 | int TileXIdx = data.RelX - this->OffsetX; 264 | int TileYIdx = data.RelY - this->OffsetY; 265 | 266 | switch (data.DataType) 267 | { 268 | case EMapboxRequestDataType::RGB: 269 | { 270 | // Get image data 271 | TArray64 RawData; 272 | const ERGBFormat InFormat = ERGBFormat::BGRA; 273 | ImageWrapper->GetRaw(InFormat, 8, RawData); 274 | 275 | // Set up offsets used to calculate correct source and destination indices for cropping 276 | int YHeightOffsetMin = this->DimY * this->MinV; 277 | int XHeightOffsetMin = this->DimX * this->MinU; 278 | int YHeightOffsetMax = ((int)(this->DimY * this->MaxV)) % this->TileDimY; 279 | int XHeightOffsetMax = ((int)(this->DimX * this->MaxU)) % this->TileDimX; 280 | 281 | int TilePixelOffsetX = -XHeightOffsetMin + TileXIdx * this->TileDimX; 282 | int TilePixelOffsetY = -YHeightOffsetMin + TileYIdx * this->TileDimY; 283 | 284 | // Iterate over y indices of source image that we want to read from (ignoring cropped indices) 285 | for (int y = TileYIdx ? 0 : YHeightOffsetMin; y < ((TileYIdx + 1 == this->MaxY && YHeightOffsetMax) ? YHeightOffsetMax : this->TileDimY); y++) 286 | { 287 | 288 | // Set y location to start writing to in destination array 289 | auto DestPtrIdx = ((int64)(y + TilePixelOffsetY)) * this->NumXHeightPixels + TilePixelOffsetX; 290 | FColor* DestPtr = &((FColor*)this->RGBData.GetData())[DestPtrIdx]; 291 | 292 | // Set y location to start reading from in source image 293 | auto SrcPtrIdx = ((int64)(y)) * this->TileDimX + (TileXIdx ? 0 : XHeightOffsetMin); 294 | const FColor* SrcPtr = &((FColor*)(RawData.GetData()))[SrcPtrIdx]; 295 | 296 | // Check that we don't aren't reading out of bounds data from source data 297 | if (SrcPtrIdx >= (256 * 256)) { 298 | UE_LOG(LogTemp, Error, TEXT("Accessing Height data out of bounds")) 299 | } 300 | 301 | // Check that we don't aren't writing to out of bounds indices in the destination array 302 | if (DestPtrIdx >= (this->NumXHeightPixels * this->NumYHeightPixels)) { 303 | UE_LOG(LogTemp, Error, TEXT("Writing Height data to out of bounds address")) 304 | } 305 | 306 | // Iterate over x indices of source image that we want to read from (ignoring cropped indices) 307 | for (int x = TileXIdx ? 0 : XHeightOffsetMin; x < ((TileXIdx + 1 == this->MaxX && XHeightOffsetMax) ? XHeightOffsetMax : this->TileDimX); x++) 308 | { 309 | *DestPtr++ = FColor(SrcPtr->R, SrcPtr->G, SrcPtr->B, SrcPtr->A); 310 | SrcPtr++; 311 | } 312 | } 313 | 314 | break; 315 | } 316 | case EMapboxRequestDataType::HEIGHT: 317 | { 318 | // Get image data 319 | TArray64 RawData; 320 | const ERGBFormat InFormat = ERGBFormat::BGRA; 321 | ImageWrapper->GetRaw(InFormat, 8, RawData); 322 | 323 | // Set up offsets used to calculate correct source and destination indices for cropping 324 | int YHeightOffsetMin = this->DimY * this->MinV; 325 | int XHeightOffsetMin = this->DimX * this->MinU; 326 | int YHeightOffsetMax = ((int)(this->DimY * this->MaxV)) % this->TileDimY; 327 | int XHeightOffsetMax = ((int)(this->DimX * this->MaxU)) % this->TileDimX; 328 | 329 | int TilePixelOffsetX = -XHeightOffsetMin + TileXIdx * this->TileDimX; 330 | int TilePixelOffsetY = -YHeightOffsetMin + TileYIdx * this->TileDimY; 331 | 332 | // Iterate over y indices of source image that we want to read from (ignoring cropped indices) 333 | for (int y = TileYIdx ? 0 : YHeightOffsetMin; y < ((TileYIdx + 1 == this->MaxY && YHeightOffsetMax) ? YHeightOffsetMax : this->TileDimY); y++) 334 | { 335 | // Set y location to start writing to in destination array 336 | auto DestPtrIdx = ((int64)(y + TilePixelOffsetY)) * this->NumXHeightPixels + TilePixelOffsetX; 337 | float* DestPtr = &((float*)this->HeightData.GetData())[DestPtrIdx]; 338 | 339 | // Set y location to start reading from in source image 340 | auto SrcPtrIdx = ((int64)(y)) * this->TileDimX + (TileXIdx ? 0 : XHeightOffsetMin); 341 | const FColor* SrcPtr = &((FColor*)(RawData.GetData()))[SrcPtrIdx]; 342 | 343 | // Check that we are not reading out of bounds data from src data 344 | if (SrcPtrIdx >= (256 * 256)) { 345 | UE_LOG(LogTemp, Error, TEXT("Accessing Height data out of bounds")) 346 | } 347 | 348 | // Check that we are not writing to out of bounds indices in the destination array 349 | if (DestPtrIdx >= (this->NumXHeightPixels * this->NumYHeightPixels)) { 350 | UE_LOG(LogTemp, Error, TEXT("Writing Height data to out of bounds address")) 351 | } 352 | 353 | // Iterate over x indices of source image that we want to read from (ignoring cropped indices) 354 | for (int x = TileXIdx ? 0 : XHeightOffsetMin; x < ((TileXIdx + 1 == this->MaxX && XHeightOffsetMax) ? XHeightOffsetMax : this->TileDimX); x++) 355 | { 356 | *DestPtr++ = -10000.0f + ((SrcPtr->R * 256 * 256 + SrcPtr->G * 256 + SrcPtr->B) * 0.1f); 357 | SrcPtr++; 358 | } 359 | } 360 | break; 361 | } 362 | default: 363 | { 364 | break; 365 | } 366 | } 367 | 368 | this->CompletedRequests++; 369 | UE_LOG(LogTemp, Log, TEXT("Completed Request, total resolved: %d"), this->CompletedRequests); 370 | 371 | // All requests complete, collate results and broadcast to success delegate 372 | if (this->CompletedRequests >= this->TotalRequests || this->bIgnoreMissingTiles) 373 | { 374 | UE_LOG(LogTemp, Log, TEXT("MAPBOX REQUEST COMPLETE")); 375 | 376 | FGISData OutData; 377 | OutData.HeightBuffer = this->HeightData; 378 | OutData.HeightBufferX = this->NumXHeightPixels; 379 | OutData.HeightBufferY = this->NumYHeightPixels; 380 | OutData.ColorBuffer = TArray((uint8*)this->RGBData.GetData(), this->RGBData.Num() * sizeof(FColor)); 381 | OutData.ColorBufferX = this->NumXHeightPixels; 382 | OutData.ColorBufferY = this->NumYHeightPixels; 383 | OutData.ProjectionWKT = UMapboxDataSource::ProjectionWKT; 384 | OutData.CornerType = ECornerCoordinateType::LatLon; 385 | OutData.UpperLeft = FVector2D(tiley2lat(this->OffsetY, this->Zoom), tilex2long(this->OffsetX, this->Zoom)); 386 | OutData.LowerRight = FVector2D(tiley2lat(this->OffsetY+this->MaxY, this->Zoom), tilex2long(this->OffsetX+this->MaxX, this->Zoom)); 387 | OutData.PixelFormat = EPixelFormat::PF_B8G8R8A8; 388 | 389 | this->OnSuccess.Broadcast(FString(), OutData); 390 | } 391 | return; 392 | } 393 | } 394 | } 395 | 396 | if (!this->hasReqFailed) 397 | { 398 | this->hasReqFailed = true; 399 | FString FailString = bSucceeded 400 | ? FString(TEXT("One or more requests failed: Unable to process image")) 401 | : FString(TEXT("One or more requests failed: Web request failed")); 402 | this->OnFailure.Broadcast(FailString, FGISData()); 403 | } 404 | } 405 | --------------------------------------------------------------------------------