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:
Rainer Leit
2026-09-16 20:51:37 +03:00
co-authored by Claude Fable 5.1
parent 0189ed9124
commit 1945ef033b
286 changed files with 1363 additions and 0 deletions
@@ -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);
};