245 lines
12 KiB
C++
245 lines
12 KiB
C++
#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<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);
|
|
}
|
|
|
|
// 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<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));
|
|
}
|
|
}
|
|
|
|
// 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<ULandscapeComponent*> Components;
|
|
Landscape->GetComponents<ULandscapeComponent>(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<ULandscapeLayerInfoObject>(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<ULandscapeLayerInfoObject>(
|
|
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;
|
|
}
|