Tooling
This commit is contained in:
@@ -2,12 +2,15 @@
|
||||
#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,
|
||||
@@ -80,6 +83,22 @@ ALandscape* ULandscapeAuthoringLibrary::CreateLandscapeFromHeightmap(UWorld* Wor
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -146,7 +165,80 @@ ALandscape* ULandscapeAuthoringLibrary::CreateLandscapeFromHeightmap(UWorld* Wor
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -40,4 +40,15 @@ public:
|
||||
static ALandscape* CreateLandscapeFromHeightmap(UWorld* World, const FString& HeightmapFile,
|
||||
const TArray<FLandscapeAuthoringWeightmap>& Weightmaps, UMaterialInterface* Material,
|
||||
FVector Location, FVector Scale, int32 WorldPartitionGridSize = 4);
|
||||
|
||||
// Creates a ULandscapeLayerInfoObject asset carrying LayerName, which is the name the landscape material
|
||||
// blends by and the name a weightmap is imported against.
|
||||
//
|
||||
// The second thing the engine exposes no Python path to, and for the same reason as the first. There is no
|
||||
// LayerInfo factory in the bindings, and ULandscapeLayerInfoObject::LayerName is VisibleAnywhere - so the
|
||||
// obvious workaround, duplicating an existing layer info, produces an asset that silently keeps the name it
|
||||
// was copied from and paints that substance wherever the new layer should be. Returns null and logs why on
|
||||
// failure; an existing asset at the path is returned rather than replaced, so this is safe to rerun.
|
||||
UFUNCTION(BlueprintCallable, Category = "Salty|Authoring")
|
||||
static ULandscapeLayerInfoObject* CreateLayerInfo(const FString& PackagePath, const FString& AssetName, FName LayerName);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user