L_World: a 200 km2 world-partitioned landscape from a seeded heightmap
Off the ladder at the user's request. Scripts/Authoring/generate_heightmap.py (numpy, installed locally by bootstrap-pylib.sh) writes a 16-bit height PNG and three 8-bit weight PNGs to Content/World/Heightmaps; create_world.py rebuilds /Game/Maps/L_World from them through ULandscapeAuthoringLibrary in the new SaltyEditor module, which wraps ALandscape::Import because the engine exposes no landscape creation to Python. Uses Elite_RockyMeadows' landscape material and layer infos (the pack itself is still uncommitted). 4033 vertices a side at 350 cm a quad; swap the PNGs and rerun to change the terrain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0189ed9124
commit
1945ef033b
@@ -12,5 +12,6 @@ public class SaltyEditorTarget : TargetRules
|
||||
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8;
|
||||
ExtraModuleNames.Add("SaltyCore");
|
||||
ExtraModuleNames.Add("Salty");
|
||||
ExtraModuleNames.Add("SaltyEditor");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "Authoring/LandscapeAuthoringLibrary.h"
|
||||
#include "SaltyEditor.h"
|
||||
#include "Landscape.h"
|
||||
#include "LandscapeInfo.h"
|
||||
#include "LandscapeImportHelper.h"
|
||||
#include "LandscapeLayerInfoObject.h"
|
||||
#include "LandscapeSubsystem.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
|
||||
ALandscape* ULandscapeAuthoringLibrary::CreateLandscapeFromHeightmap(UWorld* World, const FString& HeightmapFile,
|
||||
const TArray<FLandscapeAuthoringWeightmap>& Weightmaps, UMaterialInterface* Material,
|
||||
FVector Location, FVector Scale, int32 WorldPartitionGridSize)
|
||||
{
|
||||
if (!World)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: no world"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The heightmap decides the resolution; everything else follows it.
|
||||
FLandscapeImportDescriptor HeightDescriptor;
|
||||
FText Message;
|
||||
if (FLandscapeImportHelper::GetHeightmapImportDescriptor(HeightmapFile, /*bSingleFile*/ true, /*bFlipYAxis*/ false, HeightDescriptor, Message) == ELandscapeImportResult::Error
|
||||
|| HeightDescriptor.ImportResolutions.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: cannot read %s: %s"), *HeightmapFile, *Message.ToString());
|
||||
return nullptr;
|
||||
}
|
||||
const FLandscapeImportResolution FileResolution = HeightDescriptor.ImportResolutions[0];
|
||||
|
||||
TArray<uint16> HeightData;
|
||||
if (FLandscapeImportHelper::GetHeightmapImportData(HeightDescriptor, 0, HeightData, Message) == ELandscapeImportResult::Error)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: cannot import %s: %s"), *HeightmapFile, *Message.ToString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The same layout choice the editor makes for an imported file.
|
||||
int32 QuadsPerSection = 63;
|
||||
int32 SectionsPerComponent = 1;
|
||||
FIntPoint ComponentCount;
|
||||
FLandscapeImportHelper::ChooseBestComponentSizeForImport(FileResolution.Width, FileResolution.Height, QuadsPerSection, SectionsPerComponent, ComponentCount);
|
||||
const int32 QuadsPerComponent = QuadsPerSection * SectionsPerComponent;
|
||||
const FLandscapeImportResolution RequiredResolution(ComponentCount.X * QuadsPerComponent + 1, ComponentCount.Y * QuadsPerComponent + 1);
|
||||
|
||||
if (RequiredResolution != FileResolution)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Warning, TEXT("CreateLandscapeFromHeightmap: %ux%u does not fit a component layout; expanding to %ux%u"),
|
||||
FileResolution.Width, FileResolution.Height, RequiredResolution.Width, RequiredResolution.Height);
|
||||
TArray<uint16> Expanded;
|
||||
FLandscapeImportHelper::TransformHeightmapImportData(HeightData, Expanded, FileResolution, RequiredResolution, ELandscapeImportTransformType::ExpandCentered);
|
||||
HeightData = MoveTemp(Expanded);
|
||||
}
|
||||
|
||||
TArray<FLandscapeImportLayerInfo> LayerInfos;
|
||||
for (const FLandscapeAuthoringWeightmap& Weightmap : Weightmaps)
|
||||
{
|
||||
if (!Weightmap.LayerInfo)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Warning, TEXT("CreateLandscapeFromHeightmap: a weightmap has no layer info; skipped"));
|
||||
continue;
|
||||
}
|
||||
FLandscapeImportLayerInfo Info(Weightmap.LayerInfo->GetLayerName());
|
||||
Info.LayerInfo = Weightmap.LayerInfo;
|
||||
Info.SourceFilePath = Weightmap.File;
|
||||
|
||||
FLandscapeImportDescriptor WeightDescriptor;
|
||||
if (FLandscapeImportHelper::GetWeightmapImportDescriptor(Weightmap.File, true, false, Info.LayerName, WeightDescriptor, Message) == ELandscapeImportResult::Error
|
||||
|| WeightDescriptor.ImportResolutions.Num() == 0
|
||||
|| FLandscapeImportHelper::GetWeightmapImportData(WeightDescriptor, 0, Info.LayerName, Info.LayerData, Message) == ELandscapeImportResult::Error)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: cannot read weightmap %s: %s"), *Weightmap.File, *Message.ToString());
|
||||
return nullptr;
|
||||
}
|
||||
if (WeightDescriptor.ImportResolutions[0] != RequiredResolution)
|
||||
{
|
||||
TArray<uint8> Expanded;
|
||||
FLandscapeImportHelper::TransformWeightmapImportData(Info.LayerData, Expanded, WeightDescriptor.ImportResolutions[0], RequiredResolution, ELandscapeImportTransformType::ExpandCentered);
|
||||
Info.LayerData = MoveTemp(Expanded);
|
||||
}
|
||||
LayerInfos.Add(MoveTemp(Info));
|
||||
}
|
||||
|
||||
TMap<FGuid, TArray<uint16>> HeightDataPerLayer;
|
||||
HeightDataPerLayer.Add(FGuid(), MoveTemp(HeightData));
|
||||
TMap<FGuid, TArray<FLandscapeImportLayerInfo>> LayerDataPerLayer;
|
||||
LayerDataPerLayer.Add(FGuid(), LayerInfos);
|
||||
|
||||
// Centre the landscape on Location, as the editor centres it on the gizmo.
|
||||
const FVector Offset = FTransform(FRotator::ZeroRotator, FVector::ZeroVector, Scale)
|
||||
.TransformVector(FVector(-ComponentCount.X * QuadsPerComponent / 2.0, -ComponentCount.Y * QuadsPerComponent / 2.0, 0.0));
|
||||
ALandscape* Landscape = World->SpawnActor<ALandscape>(Location + Offset, FRotator::ZeroRotator);
|
||||
if (!Landscape)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: SpawnActor<ALandscape> failed"));
|
||||
return nullptr;
|
||||
}
|
||||
Landscape->LandscapeMaterial = Material;
|
||||
Landscape->SetActorRelativeScale3D(Scale);
|
||||
Landscape->StaticLightingLOD = FMath::DivideAndRoundUp(FMath::CeilLogTwo((RequiredResolution.Width * RequiredResolution.Height) / (2048 * 2048) + 1), (uint32)2);
|
||||
|
||||
Landscape->Import(FGuid::NewGuid(), 0, 0, RequiredResolution.Width - 1, RequiredResolution.Height - 1, SectionsPerComponent, QuadsPerSection,
|
||||
HeightDataPerLayer, *HeightmapFile, LayerDataPerLayer, ELandscapeImportAlphamapType::Additive, TArrayView<const FLandscapeLayer>());
|
||||
|
||||
ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo();
|
||||
if (!LandscapeInfo)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: Import produced no landscape info"));
|
||||
return nullptr;
|
||||
}
|
||||
Landscape->SetActorLabel(TEXT("Landscape"));
|
||||
LandscapeInfo->UpdateLayerInfoMap(Landscape);
|
||||
|
||||
for (const FLandscapeImportLayerInfo& Info : LayerInfos)
|
||||
{
|
||||
Landscape->AddTargetLayer(Info.LayerInfo->GetLayerName(), FLandscapeTargetLayerSettings(Info.LayerInfo, Info.SourceFilePath));
|
||||
const int32 LayerIndex = LandscapeInfo->GetLayerInfoIndex(Info.LayerName);
|
||||
if (LayerIndex != INDEX_NONE)
|
||||
{
|
||||
LandscapeInfo->Layers[LayerIndex].LayerInfoObj = Info.LayerInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (ULandscapeSubsystem* Subsystem = World->GetSubsystem<ULandscapeSubsystem>())
|
||||
{
|
||||
if (Subsystem->IsGridBased() && WorldPartitionGridSize > 0)
|
||||
{
|
||||
Subsystem->ChangeGridSize(LandscapeInfo, static_cast<uint32>(WorldPartitionGridSize));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogSaltyEditor, Log, TEXT("CreateLandscapeFromHeightmap: %ux%u vertices, %dx%d components of %d quads, scale (%g, %g, %g), %d paint layers"),
|
||||
RequiredResolution.Width, RequiredResolution.Height, ComponentCount.X, ComponentCount.Y, QuadsPerComponent, Scale.X, Scale.Y, Scale.Z, LayerInfos.Num());
|
||||
return Landscape;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "LandscapeAuthoringLibrary.generated.h"
|
||||
|
||||
class ALandscape;
|
||||
class ULandscapeLayerInfoObject;
|
||||
class UMaterialInterface;
|
||||
|
||||
// One paint layer to import with the heightmap: the layer info asset the material expects and an 8-bit
|
||||
// greyscale file (PNG or raw) with the same resolution as the heightmap.
|
||||
USTRUCT(BlueprintType)
|
||||
struct FLandscapeAuthoringWeightmap
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landscape")
|
||||
TObjectPtr<ULandscapeLayerInfoObject> LayerInfo = nullptr;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landscape")
|
||||
FString File;
|
||||
};
|
||||
|
||||
// What the editor's "New Landscape > Import" button does, callable from an authoring script. The engine exposes
|
||||
// no landscape creation to Python, so this is the project's first editor tool. Mirrors
|
||||
// FLandscapeEditorDetailCustomization_NewLandscape::OnCreateButtonClicked without the regions path.
|
||||
UCLASS()
|
||||
class SALTYEDITOR_API ULandscapeAuthoringLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Creates a landscape in World from a 16-bit heightmap file, centred on Location, with the given per-quad
|
||||
// scale (cm; Z is the engine's height scale where 100 spans -256 m to +256 m). The component layout is chosen
|
||||
// from the file's resolution; a file that does not fit a layout exactly is expanded, not resampled. In a
|
||||
// world-partitioned level the landscape is split into streaming proxies of GridSize x GridSize components.
|
||||
// Returns null and logs why on failure.
|
||||
UFUNCTION(BlueprintCallable, Category = "Salty|Authoring")
|
||||
static ALandscape* CreateLandscapeFromHeightmap(UWorld* World, const FString& HeightmapFile,
|
||||
const TArray<FLandscapeAuthoringWeightmap>& Weightmaps, UMaterialInterface* Material,
|
||||
FVector Location, FVector Scale, int32 WorldPartitionGridSize = 4);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
using UnrealBuildTool;
|
||||
|
||||
// Editor-only tools. Arrived with the first one (Docs/Spec/Architecture.md, Modules): the landscape authoring
|
||||
// library that Scripts/Authoring/create_world.py calls, because the engine exposes no landscape creation to Python.
|
||||
public class SaltyEditor : ModuleRules
|
||||
{
|
||||
public SaltyEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] {
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] {
|
||||
"UnrealEd",
|
||||
"Landscape",
|
||||
"LandscapeEditor" // FLandscapeImportHelper reads the heightmap and weightmap files
|
||||
});
|
||||
|
||||
PublicIncludePaths.Add(ModuleDirectory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "SaltyEditor.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, SaltyEditor);
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogSaltyEditor);
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Logging/LogMacros.h"
|
||||
|
||||
SALTYEDITOR_API DECLARE_LOG_CATEGORY_EXTERN(LogSaltyEditor, Log, All);
|
||||
Reference in New Issue
Block a user