Files
UnrealPrototyping/Source/SaltyEditor/Authoring/LandscapeAuthoringLibrary.cpp
T
Rainer LeitandClaude Fable 5.1 1945ef033b 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>
2026-09-16 20:51:37 +03:00

137 lines
6.4 KiB
C++

#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;
}