#include "Authoring/LandscapeAuthoringLibrary.h" #include "SaltyEditor.h" #include "Landscape.h" #include "LandscapeInfo.h" #include "LandscapeComponent.h" #include "LandscapeImportHelper.h" #include "LandscapeLayerInfoObject.h" #include "LandscapeSubsystem.h" #include "Engine/World.h" #include "Materials/MaterialInterface.h" #include "RenderingThread.h" #include "AssetRegistry/AssetRegistryModule.h" #include "UObject/Package.h" ALandscape* ULandscapeAuthoringLibrary::CreateLandscapeFromHeightmap(UWorld* World, const FString& HeightmapFile, const TArray& 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 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 Expanded; FLandscapeImportHelper::TransformHeightmapImportData(HeightData, Expanded, FileResolution, RequiredResolution, ELandscapeImportTransformType::ExpandCentered); HeightData = MoveTemp(Expanded); } TArray 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 Expanded; FLandscapeImportHelper::TransformWeightmapImportData(Info.LayerData, Expanded, WeightDescriptor.ImportResolutions[0], RequiredResolution, ELandscapeImportTransformType::ExpandCentered); Info.LayerData = MoveTemp(Expanded); } // An empty layer would be dropped without a word further down: ALandscapeProxy::Import only calls // SetAlphaData when `MaterialLayerInfo.LayerData.Num() > 0`, so no samples here would mean a layer that // is silently never painted. It should not be reachable - FLandscapeImportHelper's reader calls // SetNumZeroed(TotalPixels) before it decodes anything, so a descriptor that resolved at all comes back // full-sized - which is exactly why it is worth a check rather than a comment: the day it is reachable // is the day a whole world comes out shaded as the material's first layer with nothing logged anywhere. if (Info.LayerData.Num() == 0) { UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: layer %s read no samples from %s; the engine would have dropped it silently"), *Info.LayerName.ToString(), *Weightmap.File); return nullptr; } UE_LOG(LogSaltyEditor, Log, TEXT("CreateLandscapeFromHeightmap: layer %s <- %s, %d samples at %ux%u"), *Info.LayerName.ToString(), *Weightmap.File, Info.LayerData.Num(), WeightDescriptor.ImportResolutions[0].Width, WeightDescriptor.ImportResolutions[0].Height); LayerInfos.Add(MoveTemp(Info)); } TMap> HeightDataPerLayer; HeightDataPerLayer.Add(FGuid(), MoveTemp(HeightData)); TMap> 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(Location + Offset, FRotator::ZeroRotator); if (!Landscape) { UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: SpawnActor 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()); 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()) { if (Subsystem->IsGridBased() && WorldPartitionGridSize > 0) { Subsystem->ChangeGridSize(LandscapeInfo, static_cast(WorldPartitionGridSize)); } } // Edit layers are always on, and the render heightmaps and weightmaps are produced from the imported layer // data by the edit-layer merge, which normally runs on the editor tick. A commandlet never ticks, so without // this the saved level has the imported collision heights but flat, empty render textures, and the landscape // is invisible from below. The editor's own import does the same full update before saving. Landscape->ForceLayersFullUpdate(); for (int32 Attempt = 0; Attempt < 500 && !Landscape->IsUpToDate(); ++Attempt) { Landscape->TickLayers(1.0f / 30.0f); FlushRenderingCommands(); } if (!Landscape->IsUpToDate()) { UE_LOG(LogSaltyEditor, Warning, TEXT("CreateLandscapeFromHeightmap: the edit-layer update did not finish; render heightmaps may be stale")); } // Did the weights actually survive the import and the merge? A component with no weightmap allocations is a // landscape the material will shade entirely from its preview weights, which looks like a material problem // and is not one. Checked rather than assumed, because every failure mode above this line is silent. { TArray Components; Landscape->GetComponents(Components); if (Components.Num() > 0 && Components[0] != nullptr) { const int32 Allocations = Components[0]->GetWeightmapLayerAllocations().Num(); const int32 Textures = Components[0]->GetWeightmapTextures().Num(); if (Allocations == 0 && LayerInfos.Num() > 0) { UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: %d layers were imported but component 0 has no weightmap allocations; the ground will render as the material's first layer everywhere"), LayerInfos.Num()); } else { UE_LOG(LogSaltyEditor, Log, TEXT("CreateLandscapeFromHeightmap: component 0 carries %d weightmap allocation(s) across %d texture(s)"), Allocations, Textures); } } } 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; } ULandscapeLayerInfoObject* ULandscapeAuthoringLibrary::CreateLayerInfo(const FString& PackagePath, const FString& AssetName, FName LayerName) { if (PackagePath.IsEmpty() || AssetName.IsEmpty() || LayerName.IsNone()) { UE_LOG(LogSaltyEditor, Error, TEXT("CreateLayerInfo: needs a package path, an asset name and a layer name")); return nullptr; } const FString PackageName = PackagePath / AssetName; // Rerunnable: an existing layer info is returned as it is. Replacing one would break every landscape that // already paints with it, and the thing a caller wants here is "make sure this exists". if (ULandscapeLayerInfoObject* Existing = LoadObject(nullptr, *(PackageName + TEXT(".") + AssetName))) { if (Existing->GetLayerName() != LayerName) { UE_LOG(LogSaltyEditor, Warning, TEXT("CreateLayerInfo: %s already exists and its LayerName is %s, not %s; left alone"), *PackageName, *Existing->GetLayerName().ToString(), *LayerName.ToString()); } return Existing; } UPackage* Package = CreatePackage(*PackageName); if (!Package) { UE_LOG(LogSaltyEditor, Error, TEXT("CreateLayerInfo: could not create package %s"), *PackageName); return nullptr; } ULandscapeLayerInfoObject* Info = NewObject( Package, *AssetName, RF_Public | RF_Standalone | RF_Transactional); if (!Info) { UE_LOG(LogSaltyEditor, Error, TEXT("CreateLayerInfo: could not create %s"), *PackageName); return nullptr; } // The whole reason this function exists: LayerName is VisibleAnywhere, so Python cannot set it and a // duplicated layer info keeps the name it was copied from. Through the setter, not the member: the member // is deprecated and goes private next release, and bInModify false because the object was made a moment // ago and has nothing to record an undo against. Info->SetLayerName(LayerName, /*bInModify=*/false); Info->MarkPackageDirty(); FAssetRegistryModule::AssetCreated(Info); UE_LOG(LogSaltyEditor, Log, TEXT("CreateLayerInfo: %s with LayerName %s"), *PackageName, *LayerName.ToString()); return Info; }