3169 lines
109 KiB
C++
3169 lines
109 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "GenericPlatform/GenericPlatformStackWalk.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/Guid.h"
|
|
#include "Math/RandomStream.h"
|
|
#include "Stats/Stats.h"
|
|
#include "Async/AsyncWork.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "Misc/App.h"
|
|
#include "UObject/ObjectMacros.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "EngineDefines.h"
|
|
#include "Engine/EngineTypes.h"
|
|
#include "GameFramework/Actor.h"
|
|
#include "ShowFlags.h"
|
|
#include "RHI.h"
|
|
#include "RenderingThread.h"
|
|
#include "DataDrivenShaderPlatformInfo.h"
|
|
#include "ShaderParameters.h"
|
|
#include "RHIStaticStates.h"
|
|
#include "SceneView.h"
|
|
#include "Shader.h"
|
|
#include "Landscape.h"
|
|
#include "LandscapePrivate.h"
|
|
#include "LandscapeProxy.h"
|
|
#include "LandscapeInfo.h"
|
|
#include "LightMap.h"
|
|
#include "Engine/MapBuildDataRegistry.h"
|
|
#include "ShadowMap.h"
|
|
#include "LandscapeComponent.h"
|
|
#include "LandscapeSubsystem.h"
|
|
#include "LandscapeGrassMapsBuilder.h"
|
|
#include "LandscapeVersion.h"
|
|
#include "MaterialShaderType.h"
|
|
#include "MeshMaterialShaderType.h"
|
|
#include "MeshMaterialShader.h"
|
|
#include "Materials/Material.h"
|
|
#include "LandscapeGrassType.h"
|
|
#include "Materials/MaterialExpressionLandscapeGrassOutput.h"
|
|
#include "Engine/TextureRenderTarget2D.h"
|
|
#include "LandscapeDataAccess.h"
|
|
#include "StaticMeshComponentLODInfo.h"
|
|
#include "StaticMeshResources.h"
|
|
#include "LandscapeLight.h"
|
|
#include "GrassInstancedStaticMeshComponent.h"
|
|
#include "Materials/MaterialInstanceConstant.h"
|
|
#include "ShaderParameterUtils.h"
|
|
#include "EngineModule.h"
|
|
#include "LandscapeRender.h"
|
|
#include "LandscapeGrassWeightExporter.h"
|
|
#include "MaterialCompiler.h"
|
|
#include "Algo/Accumulate.h"
|
|
#include "Algo/ForEach.h"
|
|
#include "UObject/Package.h"
|
|
#include "Engine/StaticMesh.h"
|
|
#include "Components/InstancedStaticMeshComponent.h"
|
|
#include "Engine/InstancedStaticMesh.h"
|
|
#include "MeshPassProcessor.h"
|
|
#include "MeshPassProcessor.inl"
|
|
#include "Math/Halton.h"
|
|
#include "EngineUtils.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
#include "TextureCompiler.h"
|
|
#include "RenderCaptureInterface.h"
|
|
#include "SimpleMeshDrawCommandPass.h"
|
|
#include "UObject/FortniteMainBranchObjectVersion.h"
|
|
#include "MaterialCachedData.h"
|
|
#include "SceneRenderTargetParameters.h"
|
|
#include "TextureResource.h"
|
|
#include "Serialization/ArchiveCrc32.h"
|
|
#include "SceneRendererInterface.h"
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
#include "DrawDebugHelpers.h"
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
#define LOCTEXT_NAMESPACE "Landscape"
|
|
|
|
DEFINE_LOG_CATEGORY(LogGrass);
|
|
|
|
static float GGuardBandMultiplier = 1.3f;
|
|
static FAutoConsoleVariableRef CVarGuardBandMultiplier(
|
|
TEXT("grass.GuardBandMultiplier"),
|
|
GGuardBandMultiplier,
|
|
TEXT("Used to control discarding in the grass system. Approximate range, 1-4. Multiplied by the cull distance to control when we add grass components."));
|
|
|
|
static float GGuardBandDiscardMultiplier = 1.4f;
|
|
static FAutoConsoleVariableRef CVarGuardBandDiscardMultiplier(
|
|
TEXT("grass.GuardBandDiscardMultiplier"),
|
|
GGuardBandDiscardMultiplier,
|
|
TEXT("Used to control discarding in the grass system. Approximate range, 1-4. Multiplied by the cull distance to control when we discard grass components."));
|
|
|
|
static int32 GMinFramesToKeepGrass = 30;
|
|
static FAutoConsoleVariableRef CVarMinFramesToKeepGrass(
|
|
TEXT("grass.MinFramesToKeepGrass"),
|
|
GMinFramesToKeepGrass,
|
|
TEXT("Minimum number of frames before cached grass can be discarded; used to prevent thrashing."));
|
|
|
|
static int32 GGrassTickInterval = 1;
|
|
static FAutoConsoleVariableRef CVarGrassTickInterval(
|
|
TEXT("grass.TickInterval"),
|
|
GGrassTickInterval,
|
|
TEXT("Number of frames between grass ticks."));
|
|
|
|
static float GMinTimeToKeepGrass = 5.0f;
|
|
static FAutoConsoleVariableRef CVarMinTimeToKeepGrass(
|
|
TEXT("grass.MinTimeToKeepGrass"),
|
|
GMinTimeToKeepGrass,
|
|
TEXT("Minimum number of seconds before cached grass can be discarded; used to prevent thrashing."));
|
|
|
|
static int32 GMaxInstancesPerComponent = 65536;
|
|
static FAutoConsoleVariableRef CVarMaxInstancesPerComponent(
|
|
TEXT("grass.MaxInstancesPerComponent"),
|
|
GMaxInstancesPerComponent,
|
|
TEXT("Used to control the number of grass components created. More can be more efficient, but can be hitchy as new components come into range"));
|
|
|
|
static int32 GMaxAsyncTasks = 4;
|
|
static FAutoConsoleVariableRef CVarMaxAsyncTasks(
|
|
TEXT("grass.MaxAsyncTasks"),
|
|
GMaxAsyncTasks,
|
|
TEXT("Used to control the number of grass components created at a time."));
|
|
|
|
static float GGrassDensityScale = 1;
|
|
static FAutoConsoleVariableRef CVarGrassDensityScale(
|
|
TEXT("grass.densityScale"),
|
|
GGrassDensityScale,
|
|
TEXT("Multiplier on all grass densities."),
|
|
ECVF_Scalability);
|
|
|
|
float GGrassCullDistanceScale = 1;
|
|
static FAutoConsoleVariableRef CVarGrassCullDistanceScale(
|
|
TEXT("grass.CullDistanceScale"),
|
|
GGrassCullDistanceScale,
|
|
TEXT("Multiplier on all grass cull distances."),
|
|
ECVF_Scalability);
|
|
|
|
int32 GGrassEnable = 1;
|
|
static FAutoConsoleVariableRef CVarGrassEnable(
|
|
TEXT("grass.Enable"),
|
|
GGrassEnable,
|
|
TEXT("1: Enable Grass; 0: Disable Grass"));
|
|
|
|
static int32 GGrassDiscardDataOnLoad = 0;
|
|
static FAutoConsoleVariableRef CVarGrassDiscardDataOnLoad(
|
|
TEXT("grass.DiscardDataOnLoad"),
|
|
GGrassDiscardDataOnLoad,
|
|
TEXT("1: Discard grass data on load (disables grass); 0: Keep grass data (requires reloading level)"),
|
|
ECVF_Scalability);
|
|
|
|
static int32 GCullSubsections = 1;
|
|
static FAutoConsoleVariableRef CVarCullSubsections(
|
|
TEXT("grass.CullSubsections"),
|
|
GCullSubsections,
|
|
TEXT("1: Cull each foliage component; 0: Cull only based on the landscape component."));
|
|
|
|
static int32 GDisableGPUCull = 0;
|
|
static FAutoConsoleVariableRef CVarDisableGPUCull(
|
|
TEXT("grass.DisableGPUCull"),
|
|
GDisableGPUCull,
|
|
TEXT("For debugging. Set this to zero to see where the grass is generated. Useful for tweaking the guard bands."));
|
|
|
|
static int32 GDisableDynamicShadows = 0;
|
|
static FAutoConsoleVariableRef CVarDisableDynamicShadows(
|
|
TEXT("grass.DisableDynamicShadows"),
|
|
GDisableDynamicShadows,
|
|
TEXT("0: Dynamic shadows from grass follow the grass type bCastDynamicShadow flag; 1: Dynamic shadows are disabled for all grass"));
|
|
|
|
static int32 GIgnoreExcludeBoxes = 0;
|
|
static FAutoConsoleVariableRef CVarIgnoreExcludeBoxes(
|
|
TEXT("grass.IgnoreExcludeBoxes"),
|
|
GIgnoreExcludeBoxes,
|
|
TEXT("For debugging. Ignores any exclusion boxes."));
|
|
|
|
static int32 GGrassMaxCreatePerFrame = 1;
|
|
static FAutoConsoleVariableRef CVarGrassMaxCreatePerFrame(
|
|
TEXT("grass.MaxCreatePerFrame"),
|
|
GGrassMaxCreatePerFrame,
|
|
TEXT("Maximum number of Grass components to create per frame"));
|
|
|
|
static int32 GGrassCreationPrioritizedMultipler = 4;
|
|
static FAutoConsoleVariableRef CVarGrassCreationPrioritizedMultipler(
|
|
TEXT("grass.GrassCreationPrioritizedMultipler"),
|
|
GGrassCreationPrioritizedMultipler,
|
|
TEXT("Multiplier applied to MaxCreatePerFrame and MaxAsyncTasks when grass creation is prioritized."));
|
|
|
|
static int32 GGrassUpdateAllOnRebuild = 0;
|
|
static FAutoConsoleVariableRef CVarUpdateAllOnRebuild(
|
|
TEXT("grass.UpdateAllOnRebuild"),
|
|
GGrassUpdateAllOnRebuild,
|
|
TEXT(""));
|
|
|
|
static TAutoConsoleVariable<int32> CVarRayTracingLandscapeGrass(
|
|
TEXT("r.RayTracing.Geometry.LandscapeGrass"),
|
|
0,
|
|
TEXT("Include landscapes grass in ray tracing effects (default = 1)"));
|
|
|
|
const TCHAR* GGrassQualityLevelCVarName = TEXT("r.grass.DensityQualityLevel");
|
|
const TCHAR* GGrassQualityLevelScalabilitySection = TEXT("ViewDistanceQuality");
|
|
|
|
int32 GGrassQualityLevel = -1;
|
|
static FAutoConsoleVariableRef CVarGGrassDensityQualityLevelCVar(
|
|
GGrassQualityLevelCVarName,
|
|
GGrassQualityLevel,
|
|
TEXT("The quality level for grass (low, medium, high, epic). \n"),
|
|
ECVF_Scalability);
|
|
|
|
static FAutoConsoleCommand ConsoleCommandDumpLandscapeGrassData(
|
|
TEXT("grass.DumpGrassData"),
|
|
TEXT("[optional: -csv -detailed -byproxy -bycomponent -bygrasstype -full] - Dumps a report of all grass data being currently used on landscape components. \n"
|
|
"-csv formats the report in a CSV-friendly way. \n"
|
|
"-fullnames displays the listed objects' full names, rather than the user-friendly version. \n"
|
|
"-showempty will dump info even from components with no grass data \n"
|
|
"-detailed shows a detailed report of all grass data, for all grass types, in all landscape components. \n"
|
|
"-byproxy shows a report of grass data per landscape proxy. \n"
|
|
"-bycomponent shows a report of grass data per landscape component. \n"
|
|
"-bygrasstype shows a report of grass data per grass type. \n"
|
|
"-full enables all sub-reports. \n"
|
|
"If no report type option specified, assume full report is requested."),
|
|
FConsoleCommandWithArgsDelegate::CreateLambda([](const TArray<FString>& Args)
|
|
{
|
|
bool bCSV = false;
|
|
bool bUseFullNames = false;
|
|
bool bIncludeEmpty = false;
|
|
bool bDetailed = false;
|
|
bool bByProxy = false;
|
|
bool bByComponent = false;
|
|
bool bByGrassType = false;
|
|
bool bFull = false;
|
|
for (const FString& Arg : Args)
|
|
{
|
|
if (FParse::Param(*Arg, TEXT("csv")))
|
|
{
|
|
bCSV = true;
|
|
}
|
|
if (FParse::Param(*Arg, TEXT("fullnames")))
|
|
{
|
|
bUseFullNames = true;
|
|
}
|
|
if (FParse::Param(*Arg, TEXT("showempty")))
|
|
{
|
|
bIncludeEmpty = true;
|
|
}
|
|
if (FParse::Param(*Arg, TEXT("detailed")))
|
|
{
|
|
bDetailed = true;
|
|
}
|
|
if (FParse::Param(*Arg, TEXT("byproxy")))
|
|
{
|
|
bByProxy = true;
|
|
}
|
|
if (FParse::Param(*Arg, TEXT("bycomponent")))
|
|
{
|
|
bByComponent = true;
|
|
}
|
|
if (FParse::Param(*Arg, TEXT("bygrasstype")))
|
|
{
|
|
bByGrassType = true;
|
|
}
|
|
if (FParse::Param(*Arg, TEXT("full")))
|
|
{
|
|
bFull = true;
|
|
}
|
|
}
|
|
if (!bDetailed && !bByProxy && !bByComponent && !bByGrassType && !bFull)
|
|
{
|
|
// If nothing specified, assume full report :
|
|
bFull = true;
|
|
}
|
|
|
|
struct FSortDataEntry
|
|
{
|
|
FSortDataEntry(const UWorld* InWorld, const ALandscape* InActor, const ALandscapeProxy* InProxy, const ULandscapeComponent* InComponent, const ULandscapeGrassType* InGrassType, int32 InOffset)
|
|
: World(InWorld)
|
|
, Actor(InActor)
|
|
, Proxy(InProxy)
|
|
, Component(InComponent)
|
|
, GrassType(InGrassType)
|
|
, Offset(InOffset)
|
|
{}
|
|
const UWorld* World = nullptr;
|
|
const ALandscape* Actor = nullptr;
|
|
const ALandscapeProxy* Proxy = nullptr;
|
|
const ULandscapeComponent* Component = nullptr;
|
|
const ULandscapeGrassType* GrassType = nullptr; // If nullptr, this entry corresponds to height data
|
|
int32 Offset = 0;
|
|
int32 Size = 0;
|
|
};
|
|
TArray<FSortDataEntry> SortData;
|
|
TSet<const ULandscapeGrassType*> UniqueGrassTypes;
|
|
|
|
// Gather all data first :
|
|
for (TObjectIterator<const ULandscapeComponent> It(RF_ClassDefaultObject, true, EInternalObjectFlags::Garbage); It; ++It)
|
|
{
|
|
const ULandscapeComponent* Component = *It;
|
|
if (Component->GrassData->HasValidData() && (bIncludeEmpty || !Component->GrassData->HeightWeightData.IsEmpty()))
|
|
{
|
|
if (const UWorld* World = Component->GetWorld())
|
|
{
|
|
if (const ALandscapeProxy* Proxy = Component->GetLandscapeProxy())
|
|
{
|
|
if (const ALandscape* Actor = Proxy->GetLandscapeActor())
|
|
{
|
|
int32 MinWeightOffset = MAX_int32;
|
|
for (auto ItPair : Component->GrassData->WeightOffsets)
|
|
{
|
|
const ULandscapeGrassType* GrassType = ItPair.Key;
|
|
int32 GrassTypeOffset = ItPair.Value;
|
|
SortData.Add(FSortDataEntry(World, Actor, Proxy, Component, GrassType, GrassTypeOffset));
|
|
MinWeightOffset = FMath::Min(GrassTypeOffset, MinWeightOffset);
|
|
|
|
UniqueGrassTypes.Add(GrassType);
|
|
}
|
|
// Height data, if any, is at the beginning so if one the weight offsets is at index 0, then there's no height data on this component at all
|
|
int32 HeightOffset = (MinWeightOffset == 0) ? INDEX_NONE : 0;
|
|
// Always add an entry for height, even if no height data was found (makes reporting easier) :
|
|
SortData.Add(FSortDataEntry(World, Actor, Proxy, Component, /* InGrassType = */nullptr, HeightOffset));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const int32 NumEntries = SortData.Num();
|
|
if (NumEntries == 0)
|
|
{
|
|
GLog->Logf(TEXT("No grass data."));
|
|
return;
|
|
}
|
|
|
|
// Then compute the size of each landscape grass type within a component :
|
|
{
|
|
// Sort by Component then offset:
|
|
SortData.Sort([](const FSortDataEntry& InLHS, const FSortDataEntry& InRHS)
|
|
{
|
|
if (InLHS.Component == InRHS.Component)
|
|
{
|
|
return InLHS.Offset < InRHS.Offset;
|
|
}
|
|
else
|
|
{
|
|
return InLHS.Component < InRHS.Component;
|
|
}
|
|
});
|
|
|
|
// Then iterate over all entries and use the next item's offset to compute the previous item's size, component by component:
|
|
const ULandscapeComponent* CurrentComponent = SortData[0].Component;
|
|
int32 CurrentOffsetInComponent = 0;
|
|
for (int32 NextIndex = 1; NextIndex < NumEntries; ++NextIndex)
|
|
{
|
|
const FSortDataEntry& NextEntry = SortData[NextIndex];
|
|
if (NextEntry.Component != CurrentComponent)
|
|
{
|
|
SortData[NextIndex - 1].Size = CurrentComponent->GrassData->HeightWeightData.Num() - CurrentOffsetInComponent;
|
|
CurrentComponent = NextEntry.Component;
|
|
CurrentOffsetInComponent = 0;
|
|
}
|
|
else
|
|
{
|
|
SortData[NextIndex - 1].Size = NextEntry.Offset - CurrentOffsetInComponent;
|
|
CurrentOffsetInComponent = NextEntry.Offset;
|
|
}
|
|
}
|
|
|
|
// Since the loop sets the size at index - 1, the last item's size is not yet computed :
|
|
SortData[NumEntries - 1].Size = CurrentComponent->GrassData->HeightWeightData.Num() - CurrentOffsetInComponent;
|
|
}
|
|
|
|
// Sort by World, then Actor, then Proxy, then Component, then GrassType :
|
|
SortData.Sort([](const FSortDataEntry& InLHS, const FSortDataEntry& InRHS)
|
|
{
|
|
if (InLHS.World == InRHS.World)
|
|
{
|
|
if (InLHS.Actor == InRHS.Actor)
|
|
{
|
|
if (InLHS.Proxy == InRHS.Proxy)
|
|
{
|
|
if (InLHS.Component == InRHS.Component)
|
|
{
|
|
return InLHS.GrassType < InRHS.GrassType;
|
|
}
|
|
else
|
|
{
|
|
return InLHS.Component < InRHS.Component;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return InLHS.Proxy < InRHS.Proxy;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return InLHS.Actor < InRHS.Actor;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return InLHS.World < InRHS.World;
|
|
}
|
|
});
|
|
|
|
auto GetFormattedObjectName = [bUseFullNames](const UObject* InObject) -> FString { return bUseFullNames ? InObject->GetFullName() : InObject->GetName(); };
|
|
auto GetFormattedActorName = [bUseFullNames](const AActor* InActor) -> FString { return bUseFullNames ? InActor->GetFullName() : InActor->GetActorNameOrLabel(); };
|
|
auto GetFormattedGrassTypeName = [bUseFullNames](const ULandscapeGrassType* InGrassType) -> FString { return (InGrassType != nullptr) ? (bUseFullNames ? InGrassType->GetFullName() : InGrassType->GetName()) : FString(TEXT("(HeightData)")); };
|
|
auto LogWorldActorLine = [GetFormattedObjectName, GetFormattedActorName](const UWorld* InWorld, const ALandscape* InActor) { GLog->Logf(TEXT("-- World: %s - Landscape Actor: %s --"), *GetFormattedObjectName(InWorld), *GetFormattedActorName(InActor)); };
|
|
|
|
// List of lines to display in a sub-report (one element per entry):
|
|
TArray<TPair<FString, FString>> CurrentSubReportContent; // Key = entry's name (for sorting), Value = line
|
|
auto AddLineToSubReport = [&CurrentSubReportContent](const FString& InEntryName, const FString& InLine) { CurrentSubReportContent.Emplace(InEntryName, InLine); };
|
|
auto FinalizeSubReport = [&CurrentSubReportContent]()
|
|
{
|
|
// Lexicographically sort entries, output them and reset the sub-report content :
|
|
CurrentSubReportContent.Sort([](const TPair<FString, FString>& InLHS, const TPair<FString, FString>& InRHS) { return (InLHS.Key.Compare(InRHS.Key) < 0); });
|
|
Algo::ForEach(CurrentSubReportContent, [](const TPair<FString, FString>& InEntry) { GLog->Log(*InEntry.Value); });
|
|
CurrentSubReportContent.Empty();
|
|
};
|
|
|
|
if (bFull || bDetailed)
|
|
{
|
|
// Start a new report :
|
|
GLog->Logf(TEXT("Detailed report:"));
|
|
ON_SCOPE_EXIT{ GLog->Logf(TEXT("--------")); };
|
|
|
|
if (bCSV)
|
|
{
|
|
GLog->Logf(TEXT("World,Landscape Actor,Landscape Proxy,Landscape Component,Landscape Grass Type,Size (KB)"));
|
|
for (const FSortDataEntry& Entry : SortData)
|
|
{
|
|
if ((Entry.GrassType != nullptr) || (bIncludeEmpty || (Entry.Size > 0)))
|
|
{
|
|
FString Line = FString::Printf(TEXT("%s,%s,%s,%s,%s,%.2f"),
|
|
*GetFormattedObjectName(Entry.World),
|
|
*GetFormattedActorName(Entry.Actor),
|
|
*GetFormattedActorName(Entry.Proxy),
|
|
*Entry.Component->GetName(),
|
|
*GetFormattedGrassTypeName(Entry.GrassType),
|
|
Entry.Size / 1024.0f);
|
|
// Use the line itself as the sort key since it's already formatted the way we want:
|
|
AddLineToSubReport(Line, Line);
|
|
}
|
|
}
|
|
FinalizeSubReport();
|
|
}
|
|
else
|
|
{
|
|
const UWorld* CurrentWorld = SortData[0].World;
|
|
const ALandscape* CurrentActor = SortData[0].Actor;
|
|
const ALandscapeProxy* CurrentProxy = SortData[0].Proxy;
|
|
const ULandscapeComponent* CurrentComponent = SortData[0].Component;
|
|
bool bCurrentWorldHasGrassData = false;
|
|
bool bCurrentActorHasGrassData = false;
|
|
bool bCurrentProxyHasGrassData = false;
|
|
bool bCurrentComponentHasGrassData = false;
|
|
int32 NumWorldsWithGrassData = 0;
|
|
int32 NumActorsWithGrassData = 0;
|
|
int32 NumProxiesWithGrassData = 0;
|
|
int32 NumComponentsWithGrassData = 0;
|
|
int32 TotalHeightDataSize = 0;
|
|
int32 TotalWeightDataSize = 0;
|
|
for (int32 Index = 0; Index < NumEntries; ++Index)
|
|
{
|
|
const FSortDataEntry& Entry = SortData[Index];
|
|
if (CurrentWorld != Entry.World)
|
|
{
|
|
// This world was fully processed, take note of the info and start tracking the new world:
|
|
if (bCurrentWorldHasGrassData)
|
|
{
|
|
++NumWorldsWithGrassData;
|
|
}
|
|
CurrentWorld = Entry.World;
|
|
bCurrentWorldHasGrassData = false;
|
|
}
|
|
|
|
if (CurrentActor != Entry.Actor)
|
|
{
|
|
// This actor was fully processed, take note of the info and start tracking the new actor:
|
|
if (bCurrentActorHasGrassData)
|
|
{
|
|
++NumActorsWithGrassData;
|
|
}
|
|
CurrentActor = Entry.Actor;
|
|
bCurrentActorHasGrassData = false;
|
|
}
|
|
|
|
if (CurrentProxy != Entry.Proxy)
|
|
{
|
|
// This proxy was fully processed, take note of the info and start tracking the new proxy:
|
|
if (bCurrentProxyHasGrassData)
|
|
{
|
|
++NumProxiesWithGrassData;
|
|
}
|
|
CurrentProxy = Entry.Proxy;
|
|
bCurrentProxyHasGrassData = false;
|
|
}
|
|
|
|
// This component was fully processed, take note of the info and start tracking the new component:
|
|
if (CurrentComponent != Entry.Component)
|
|
{
|
|
if (bCurrentComponentHasGrassData)
|
|
{
|
|
++NumComponentsWithGrassData;
|
|
}
|
|
CurrentComponent = Entry.Component;
|
|
bCurrentComponentHasGrassData = false;
|
|
}
|
|
|
|
if (Entry.Size > 0)
|
|
{
|
|
bCurrentWorldHasGrassData = true;
|
|
bCurrentActorHasGrassData = true;
|
|
bCurrentProxyHasGrassData = true;
|
|
bCurrentComponentHasGrassData = true;
|
|
if (Entry.GrassType == nullptr)
|
|
{
|
|
TotalHeightDataSize += Entry.Size;
|
|
}
|
|
else
|
|
{
|
|
TotalWeightDataSize += Entry.Size;
|
|
}
|
|
}
|
|
}
|
|
|
|
// And dump one more time, for the last entry, which hasn't been taken into account yet :
|
|
if (bCurrentWorldHasGrassData)
|
|
{
|
|
++NumWorldsWithGrassData;
|
|
}
|
|
if (bCurrentActorHasGrassData)
|
|
{
|
|
++NumActorsWithGrassData;
|
|
}
|
|
if (bCurrentProxyHasGrassData)
|
|
{
|
|
++NumProxiesWithGrassData;
|
|
}
|
|
if (bCurrentComponentHasGrassData)
|
|
{
|
|
++NumComponentsWithGrassData;
|
|
}
|
|
|
|
GLog->Logf(TEXT("Num worlds : %i"), NumWorldsWithGrassData);
|
|
GLog->Logf(TEXT("Num landscape actors with grass data: %i"), NumActorsWithGrassData);
|
|
GLog->Logf(TEXT("Num landscape proxies with grass data: %i"), NumProxiesWithGrassData);
|
|
GLog->Logf(TEXT("Num landscape components with grass data: %i"), NumComponentsWithGrassData);
|
|
GLog->Logf(TEXT("Num grass types used: %i"), UniqueGrassTypes.Num());
|
|
GLog->Logf(TEXT("Total height data size (KB): %.2f"), TotalHeightDataSize / 1024.0f);
|
|
GLog->Logf(TEXT("Total weight data size (KB): %.2f"), TotalWeightDataSize / 1024.0f);
|
|
GLog->Logf(TEXT("Total grass data size (KB): %.2f"), (TotalHeightDataSize + TotalWeightDataSize) / 1024.0f);
|
|
}
|
|
}
|
|
|
|
// Assumes the list is sorted by world / by actor / by proxy already:
|
|
if (bFull || bByProxy)
|
|
{
|
|
// Start a new report :
|
|
GLog->Logf(TEXT("By proxy report:"));
|
|
ON_SCOPE_EXIT{ GLog->Logf(TEXT("--------")); };
|
|
|
|
// Start a new sub-report per world / per actor:
|
|
LogWorldActorLine(SortData[0].World, SortData[0].Actor);
|
|
const ALandscape* CurrentActor = SortData[0].Actor;
|
|
|
|
auto LogCSVHeader = [bCSV]() { if (bCSV) { GLog->Logf(TEXT("Landscape Proxy,Num Grass Types,Total Height Data Size (KB), Total Weight Data Size (KB), Total Data Size (KB)")); } };
|
|
auto LogLine = [bCSV, AddLineToSubReport](const TCHAR* InProxyName, int32 InNumGrassTypes, int32 InTotalHeightDataSizeBytes, int32 InTotalWeightDataSizeBytes)
|
|
{
|
|
if (bCSV)
|
|
{
|
|
AddLineToSubReport(FString(InProxyName), FString::Printf(TEXT("%s,%i,%.2f,%.2f,%.2f"),
|
|
InProxyName,
|
|
InNumGrassTypes,
|
|
InTotalHeightDataSizeBytes / 1024.0f,
|
|
InTotalWeightDataSizeBytes / 1024.0f,
|
|
(InTotalHeightDataSizeBytes + InTotalWeightDataSizeBytes) / 1024.0f));
|
|
}
|
|
else
|
|
{
|
|
AddLineToSubReport(FString(InProxyName), FString::Printf(TEXT("%s: Num grass types = %i, total height data size = %.2f KB, total weight data size = %.2f KB, total data size = %.2f KB"),
|
|
InProxyName,
|
|
InNumGrassTypes,
|
|
InTotalHeightDataSizeBytes / 1024.0f,
|
|
InTotalWeightDataSizeBytes / 1024.0f,
|
|
(InTotalHeightDataSizeBytes + InTotalWeightDataSizeBytes) / 1024.0f));
|
|
}
|
|
};
|
|
|
|
LogCSVHeader();
|
|
const ALandscapeProxy* CurrentProxy = SortData[0].Proxy;
|
|
int32 HeightDataSizeForCurrentProxy = 0;
|
|
int32 WeightDataSizeForCurrentProxy = 0;
|
|
TSet<const ULandscapeGrassType*> UniqueGrassTypesForCurrentProxy;
|
|
for (int32 Index = 0; Index < NumEntries; ++Index)
|
|
{
|
|
const FSortDataEntry& Entry = SortData[Index];
|
|
if (Entry.Proxy != CurrentProxy)
|
|
{
|
|
// The info from CurrentProxy is now complete, we can dump it :
|
|
LogLine(*GetFormattedActorName(CurrentProxy), UniqueGrassTypesForCurrentProxy.Num(), HeightDataSizeForCurrentProxy, WeightDataSizeForCurrentProxy);
|
|
|
|
// Start gathering stats for the next proxy :
|
|
CurrentProxy = Entry.Proxy;
|
|
HeightDataSizeForCurrentProxy = 0;
|
|
WeightDataSizeForCurrentProxy = 0;
|
|
UniqueGrassTypesForCurrentProxy.Empty();
|
|
}
|
|
|
|
if (Entry.Actor != CurrentActor)
|
|
{
|
|
// Start a new sub-report per world / per actor:
|
|
FinalizeSubReport();
|
|
LogWorldActorLine(Entry.World, Entry.Actor);
|
|
LogCSVHeader();
|
|
CurrentActor = Entry.Actor;
|
|
}
|
|
|
|
// Accumulate data about this proxy :
|
|
if (Entry.Size > 0)
|
|
{
|
|
if (Entry.GrassType != nullptr)
|
|
{
|
|
WeightDataSizeForCurrentProxy += Entry.Size;
|
|
UniqueGrassTypesForCurrentProxy.Add(Entry.GrassType);
|
|
}
|
|
else
|
|
{
|
|
HeightDataSizeForCurrentProxy += Entry.Size;
|
|
}
|
|
}
|
|
}
|
|
|
|
// And dump one more time, for the last proxy, which hasn't been reported yet :
|
|
LogLine(*GetFormattedActorName(CurrentProxy), UniqueGrassTypesForCurrentProxy.Num(), HeightDataSizeForCurrentProxy, WeightDataSizeForCurrentProxy);
|
|
FinalizeSubReport();
|
|
}
|
|
|
|
// Assumes the list is sorted by world / by actor / ... / by component already:
|
|
if (bFull || bByComponent)
|
|
{
|
|
// Start a new report :
|
|
GLog->Logf(TEXT("By component report:"));
|
|
ON_SCOPE_EXIT{ GLog->Logf(TEXT("--------")); };
|
|
|
|
// Start a new sub-report per world / per actor:
|
|
LogWorldActorLine(SortData[0].World, SortData[0].Actor);
|
|
const ALandscape* CurrentActor = SortData[0].Actor;
|
|
|
|
auto LogCSVHeader = [bCSV]() { if (bCSV) { GLog->Logf(TEXT("Landscape Proxy,Landscape Component,Num Grass Types,Total Height Data Size (KB), Total Weight Data Size (KB), Total Data Size (KB)")); } };
|
|
auto LogLine = [bCSV, AddLineToSubReport](const TCHAR* InProxyName, const TCHAR* InComponentName, int32 InNumGrassTypes, int32 InTotalHeightDataSizeBytes, int32 InTotalWeightDataSizeBytes)
|
|
{
|
|
if (bCSV)
|
|
{
|
|
AddLineToSubReport(FString(InComponentName), FString::Printf(TEXT("%s,%s,%i,%.2f,%.2f,%.2f"),
|
|
InProxyName,
|
|
InComponentName,
|
|
InNumGrassTypes,
|
|
InTotalHeightDataSizeBytes / 1024.0f,
|
|
InTotalWeightDataSizeBytes / 1024.0f,
|
|
(InTotalHeightDataSizeBytes + InTotalWeightDataSizeBytes) / 1024.0f));
|
|
}
|
|
else
|
|
{
|
|
AddLineToSubReport(FString(InComponentName), FString::Printf(TEXT("%s (%s): Num grass types = %i, total height data size = %.2f KB, total weight data size = %.2f KB, total data size = %.2f KB"),
|
|
InProxyName,
|
|
InComponentName,
|
|
InNumGrassTypes,
|
|
InTotalHeightDataSizeBytes / 1024.0f,
|
|
InTotalWeightDataSizeBytes / 1024.0f,
|
|
(InTotalHeightDataSizeBytes + InTotalWeightDataSizeBytes) / 1024.0f));
|
|
}
|
|
};
|
|
|
|
LogCSVHeader();
|
|
const ULandscapeComponent* CurrentComponent = SortData[0].Component;
|
|
int32 NumGrassTypesForCurrentComponent = 0;
|
|
int32 HeightDataSizeForCurrentComponent = 0;
|
|
int32 WeightDataSizeForCurrentComponent = 0;
|
|
for (int32 Index = 0; Index < NumEntries; ++Index)
|
|
{
|
|
const FSortDataEntry& Entry = SortData[Index];
|
|
if (Entry.Component != CurrentComponent)
|
|
{
|
|
// The info from CurrentComponent is now complete, we can dump it :
|
|
LogLine(*GetFormattedActorName(CurrentComponent->GetLandscapeProxy()), *CurrentComponent->GetName(), NumGrassTypesForCurrentComponent, HeightDataSizeForCurrentComponent, WeightDataSizeForCurrentComponent);
|
|
|
|
// Start gathering stats for the next component :
|
|
CurrentComponent = Entry.Component;
|
|
NumGrassTypesForCurrentComponent = 0;
|
|
HeightDataSizeForCurrentComponent = 0;
|
|
WeightDataSizeForCurrentComponent = 0;
|
|
}
|
|
|
|
if (Entry.Actor != CurrentActor)
|
|
{
|
|
// Start a new sub-report per world / per actor:
|
|
FinalizeSubReport();
|
|
LogWorldActorLine(Entry.World, Entry.Actor);
|
|
LogCSVHeader();
|
|
CurrentActor = Entry.Actor;
|
|
}
|
|
|
|
// Accumulate data about this component :
|
|
if (Entry.Size > 0)
|
|
{
|
|
if (Entry.GrassType != nullptr)
|
|
{
|
|
++NumGrassTypesForCurrentComponent;
|
|
WeightDataSizeForCurrentComponent += Entry.Size;
|
|
}
|
|
else
|
|
{
|
|
HeightDataSizeForCurrentComponent += Entry.Size;
|
|
}
|
|
}
|
|
}
|
|
|
|
// And dump one more time, for the last component, which hasn't been reported yet :
|
|
LogLine(*GetFormattedActorName(CurrentComponent->GetLandscapeProxy()), *CurrentComponent->GetName(), NumGrassTypesForCurrentComponent, HeightDataSizeForCurrentComponent, WeightDataSizeForCurrentComponent);
|
|
FinalizeSubReport();
|
|
}
|
|
|
|
if (bFull || bByGrassType)
|
|
{
|
|
// Sort by Grass Type, then Component:
|
|
SortData.Sort([](const FSortDataEntry& InLHS, const FSortDataEntry& InRHS)
|
|
{
|
|
if (InLHS.GrassType == InRHS.GrassType)
|
|
{
|
|
return (InLHS.Component < InRHS.Component);
|
|
}
|
|
return (InLHS.GrassType < InRHS.GrassType);
|
|
});
|
|
|
|
// Start a new sub-report :
|
|
GLog->Logf(TEXT("By grass type report:"));
|
|
ON_SCOPE_EXIT{ GLog->Logf(TEXT("--------")); };
|
|
|
|
auto LogCSVHeader = [bCSV]() { if (bCSV) { GLog->Logf(TEXT("Grass Type,Num Components,Total Data Size (KB)")); } };
|
|
auto LogLine = [bCSV, AddLineToSubReport](const TCHAR* InGrassTypeName, int32 InNumComponents, int32 InTotalDataSizeBytes)
|
|
{
|
|
if (bCSV)
|
|
{
|
|
AddLineToSubReport(FString(InGrassTypeName), FString::Printf(TEXT("%s,%i,%.2f"),
|
|
InGrassTypeName,
|
|
InNumComponents,
|
|
InTotalDataSizeBytes / 1024.0f));
|
|
}
|
|
else
|
|
{
|
|
AddLineToSubReport(FString(InGrassTypeName), FString::Printf(TEXT("%s: Num components = %i, total data size = %.2f KB"),
|
|
InGrassTypeName,
|
|
InNumComponents,
|
|
InTotalDataSizeBytes / 1024.0f));
|
|
}
|
|
};
|
|
|
|
LogCSVHeader();
|
|
const ULandscapeGrassType* CurrentGrassType = SortData[0].GrassType;
|
|
int32 DataSizeForCurrentGrassType = 0;
|
|
int32 NumComponentsForCurrentGrassType = 0;
|
|
for (int32 Index = 0; Index < NumEntries; ++Index)
|
|
{
|
|
const FSortDataEntry& Entry = SortData[Index];
|
|
if (Entry.GrassType != CurrentGrassType)
|
|
{
|
|
// The info from CurrentGrassType is now complete, we can dump it :
|
|
LogLine(*GetFormattedGrassTypeName(CurrentGrassType), NumComponentsForCurrentGrassType, DataSizeForCurrentGrassType);
|
|
|
|
// Start gathering stats for the next grass type :
|
|
CurrentGrassType = Entry.GrassType;
|
|
DataSizeForCurrentGrassType = 0;
|
|
NumComponentsForCurrentGrassType = 0;
|
|
}
|
|
|
|
// Accumulate data about this grass type :
|
|
if (Entry.Size > 0)
|
|
{
|
|
DataSizeForCurrentGrassType += Entry.Size;
|
|
++NumComponentsForCurrentGrassType;
|
|
}
|
|
}
|
|
|
|
// And dump one more time, for the last grass type, which hasn't been reported yet :
|
|
LogLine(*GetFormattedGrassTypeName(CurrentGrassType), NumComponentsForCurrentGrassType, DataSizeForCurrentGrassType);
|
|
FinalizeSubReport();
|
|
}
|
|
}));
|
|
|
|
#if !UE_BUILD_SHIPPING
|
|
static bool bGGrassDrawExclusionVolumes = false;
|
|
static FAutoConsoleVariableRef CVarGrassDrawExclusionVolumes(
|
|
TEXT("grass.DrawExclusionVolumes"),
|
|
bGGrassDrawExclusionVolumes,
|
|
TEXT("Whether we should draw the exclusion volumes or not"));
|
|
#endif // !UE_BUILD_SHIPPING
|
|
|
|
struct FPerQualityLevelInt;
|
|
struct FPerQualityLevelFloat;
|
|
|
|
DECLARE_CYCLE_STAT(TEXT("Grass Async Build Time"), STAT_FoliageGrassAsyncBuildTime, STATGROUP_Foliage);
|
|
DECLARE_CYCLE_STAT(TEXT("Grass Start Comp"), STAT_FoliageGrassStartComp, STATGROUP_Foliage);
|
|
DECLARE_CYCLE_STAT(TEXT("Grass End Comp"), STAT_FoliageGrassEndComp, STATGROUP_Foliage);
|
|
DECLARE_CYCLE_STAT(TEXT("Grass Destroy Comps"), STAT_FoliageGrassDestoryComp, STATGROUP_Foliage);
|
|
DECLARE_CYCLE_STAT(TEXT("Grass Update"), STAT_GrassUpdate, STATGROUP_Foliage);
|
|
DECLARE_CYCLE_STAT(TEXT("Grass Type Update Summary"), STAT_UpdateGrassTypeSummary, STATGROUP_Foliage);
|
|
DECLARE_DWORD_ACCUMULATOR_STAT(TEXT("Grass Exclusion Volumes"), STAT_GrassExclusionVolumes, STATGROUP_Foliage);
|
|
|
|
int32 ALandscapeProxy::GrassUpdateInterval = 1;
|
|
|
|
static void GrassCVarSinkFunction()
|
|
{
|
|
static float CachedGrassDensityScale = 1.0f;
|
|
float GrassDensityScale = GGrassDensityScale;
|
|
|
|
if (FApp::IsGame())
|
|
{
|
|
ALandscapeProxy::SetGrassUpdateInterval(FMath::Clamp<int32>(GGrassTickInterval, 1, 60));
|
|
}
|
|
|
|
static float CachedGrassCullDistanceScale = 1.0f;
|
|
float GrassCullDistanceScale = GGrassCullDistanceScale;
|
|
|
|
static const IConsoleVariable* DetailModeCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DetailMode"));
|
|
static int32 CachedDetailMode = DetailModeCVar ? DetailModeCVar->GetInt() : 0;
|
|
int32 DetailMode = DetailModeCVar ? DetailModeCVar->GetInt() : 0;
|
|
|
|
static const IConsoleVariable* NaniteEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Nanite"));
|
|
static int32 CachedNaniteEnabled = NaniteEnabledCVar ? NaniteEnabledCVar->GetInt() : 0;
|
|
int32 NaniteEnabled = NaniteEnabledCVar ? NaniteEnabledCVar->GetInt() : 0;
|
|
|
|
if (DetailMode != CachedDetailMode ||
|
|
GrassDensityScale != CachedGrassDensityScale ||
|
|
GrassCullDistanceScale != CachedGrassCullDistanceScale ||
|
|
CachedNaniteEnabled != NaniteEnabled)
|
|
{
|
|
CachedGrassDensityScale = GrassDensityScale;
|
|
CachedGrassCullDistanceScale = GrassCullDistanceScale;
|
|
CachedDetailMode = DetailMode;
|
|
CachedNaniteEnabled = NaniteEnabled;
|
|
|
|
for (UWorld* World : TObjectRange<UWorld>(RF_ClassDefaultObject | RF_ArchetypeObject, true, EInternalObjectFlags::Garbage))
|
|
{
|
|
if (!World->bIsTearingDown)
|
|
{
|
|
if (ULandscapeSubsystem* LandscapeSubsystem = World->GetSubsystem<ULandscapeSubsystem>())
|
|
{
|
|
LandscapeSubsystem->RegenerateGrass(/*bInFlushGrass = */true, /*bInForceSync = */true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static FAutoConsoleVariableSink CVarGrassSink(FConsoleCommandDelegate::CreateStatic(&GrassCVarSinkFunction));
|
|
|
|
//
|
|
// Grass weightmap rendering
|
|
//
|
|
|
|
FLandscapeComponentGrassData::FLandscapeComponentGrassData(ULandscapeComponent* Component)
|
|
{
|
|
#if WITH_EDITOR
|
|
GenerationHash = Component->ComputeGrassMapGenerationHash();
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
void ULandscapeComponent::InvalidateGrassTypeSummary()
|
|
{
|
|
GrassTypeSummary.bInvalid = true;
|
|
GetLandscapeProxy()->InvalidateGrassTypeSummary();
|
|
}
|
|
|
|
bool ULandscapeComponent::UpdateGrassTypes(bool bForceUpdate)
|
|
{
|
|
bool bChanged = false;
|
|
#if WITH_EDITOR
|
|
if (UMaterialInterface* Material = GetLandscapeMaterial())
|
|
{
|
|
uint32 CurMaterialAllStateCRC = Material->ComputeAllStateCRC();
|
|
if (bForceUpdate || (LastLandscapeMaterialAllStateCRCWhenGrassTypesBuilt != CurMaterialAllStateCRC))
|
|
{
|
|
LastLandscapeMaterialAllStateCRCWhenGrassTypesBuilt = CurMaterialAllStateCRC;
|
|
|
|
SetGrassTypes(Material->GetMaterial()->GetCachedExpressionData().GrassTypes);
|
|
bChanged = true;
|
|
}
|
|
}
|
|
#else
|
|
// In cooked builds, we don't automatically respond to material changes.
|
|
// The cooked value of the component's GrassTypes array is considered authoritative.
|
|
if (bForceUpdate)
|
|
{
|
|
if (UMaterialInterface* Material = GetLandscapeMaterial())
|
|
{
|
|
SetGrassTypes(Material->GetMaterial()->GetCachedExpressionData().GrassTypes);
|
|
bChanged = true;
|
|
}
|
|
}
|
|
#endif // WITH_EDITOR
|
|
return bChanged;
|
|
}
|
|
|
|
bool ULandscapeComponent::CanRenderGrassMap() const
|
|
{
|
|
// Check we can render
|
|
UWorld* ComponentWorld = GetWorld();
|
|
if (!GIsEditor || GUsingNullRHI || !ComponentWorld || ComponentWorld->IsGameWorld() || ComponentWorld->GetFeatureLevel() < ERHIFeatureLevel::SM5 || !SceneProxy)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UMaterialInstance* MaterialInstance = GetMaterialInstanceCount(false) > 0 ? GetMaterialInstance(0) : nullptr;
|
|
FMaterialResource* MaterialResource = MaterialInstance != nullptr ? MaterialInstance->GetMaterialResource(ComponentWorld->GetFeatureLevel()) : nullptr;
|
|
|
|
// Check we can render the material
|
|
if (MaterialResource == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// We only need the GrassWeight shaders on the fixed grid vertex factory to render grass maps :
|
|
FMaterialShaderTypes ShaderTypes;
|
|
UE::Landscape::Grass::AddGrassWeightShaderTypes(ShaderTypes);
|
|
|
|
FVertexFactoryType* LandscapeGrassVF = FindVertexFactoryType(FName(TEXT("FLandscapeFixedGridVertexFactory"), FNAME_Find));
|
|
if (!MaterialResource->HasShaders(ShaderTypes, LandscapeGrassVF))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
|
|
namespace UE::Landscape
|
|
{
|
|
extern uint32 ComputeGrassMapGenerationHash(const ULandscapeComponent* Component, UMaterialInterface* Material);
|
|
extern void SubmitGPUCommands(bool bBlockUntilRTComplete, bool bBlockRTUntilGPUComplete);
|
|
}
|
|
|
|
uint32 ULandscapeComponent::ComputeGrassMapGenerationHash() const
|
|
{
|
|
return UE::Landscape::ComputeGrassMapGenerationHash(this, GetLandscapeMaterial());
|
|
}
|
|
|
|
bool ULandscapeComponent::IsGrassMapOutdated() const
|
|
{
|
|
return GrassData->HasValidData() && (ComputeGrassMapGenerationHash() != GrassData->GenerationHash);
|
|
}
|
|
|
|
TArray<uint16> ULandscapeComponent::RenderWPOHeightmap(int32 LOD)
|
|
{
|
|
TArray<uint16> Results;
|
|
|
|
if (ensure(SceneProxy))
|
|
{
|
|
if (!CanRenderGrassMap())
|
|
{
|
|
GetMaterialInstance(0)->GetMaterialResource(GetWorld()->GetFeatureLevel())->FinishCompilation();
|
|
|
|
if (!CanRenderGrassMap())
|
|
{
|
|
UE_LOG(LogGrass, Verbose, TEXT("Failed to calculate Landscape WPO height for static lighting. Grass map generation shader could not be compiled."));
|
|
return Results;
|
|
}
|
|
}
|
|
|
|
if (LOD == 0)
|
|
{
|
|
FLandscapeGrassWeightExporter Exporter(GetLandscapeProxy(), { this }, /*bInNeedsGrassmap = */ false, /*bInNeedsHeightmap =*/ true, {});
|
|
|
|
while (!Exporter.IsAsyncReadbackComplete())
|
|
{
|
|
UE::Landscape::SubmitGPUCommands(/* bBlockUntilRTComplete = */ true, /* bBlockRTUntilGPUComplete = */ true);
|
|
bool bOutRenderCommandsQueued;
|
|
Exporter.CheckAndUpdateAsyncReadback(bOutRenderCommandsQueued, true);
|
|
}
|
|
|
|
TMap<ULandscapeComponent*, TUniquePtr<FLandscapeComponentGrassData>, TInlineSetAllocator<1>> TempGrassData;
|
|
TempGrassData = Exporter.FetchResults(/* bFreeAsyncReadback= */ true);
|
|
Results = TArray<uint16>(TempGrassData[this]->GetHeightData());
|
|
}
|
|
else
|
|
{
|
|
TArray<int32> HeightMips;
|
|
HeightMips.Add(LOD);
|
|
FLandscapeGrassWeightExporter Exporter(GetLandscapeProxy(), { this }, /*bInNeedsGrassmap = */ false, /*bInNeedsHeightmap =*/ false, MoveTemp(HeightMips));
|
|
|
|
while (!Exporter.IsAsyncReadbackComplete())
|
|
{
|
|
UE::Landscape::SubmitGPUCommands(/* bBlockUntilRTComplete = */ true, /* bBlockRTUntilGPUComplete = */ true);
|
|
bool bOutRenderCommandsQueued;
|
|
Exporter.CheckAndUpdateAsyncReadback(bOutRenderCommandsQueued, true);
|
|
}
|
|
|
|
TMap<ULandscapeComponent*, TUniquePtr<FLandscapeComponentGrassData>, TInlineSetAllocator<1>> TempGrassData;
|
|
TempGrassData = Exporter.FetchResults(/* bFreeAsyncReadback= */ true);
|
|
Results = MoveTemp(TempGrassData[this]->HeightMipData[LOD]);
|
|
}
|
|
}
|
|
|
|
return Results;
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
void ULandscapeComponent::RemoveGrassMap()
|
|
{
|
|
// this does a thread safe replacement of the existing grassdata with a newly allocated empty (invalid) one
|
|
// this ensures if anyone else is accessing the old grassdata (while holding a shared ref to it), it won't be modified or deleted
|
|
// this is also important for PIE, which can share grassdatas with editor
|
|
GrassData = MakeShared<FLandscapeComponentGrassData>();
|
|
}
|
|
|
|
void ALandscapeProxy::UpdateGrassTypeSummary()
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_UpdateGrassTypeSummary);
|
|
|
|
bool bGrassQualityLevelEnabled = GEngine && GEngine->UseGrassVarityPerQualityLevels;
|
|
|
|
bool bProxyHasAnyGrass = false;
|
|
double ProxyMaxGrassCullDistance = 0.0;
|
|
for (ULandscapeComponent* Component : LandscapeComponents)
|
|
{
|
|
double ComponentMaxGrassCullDistance;
|
|
int32 ComponentGrassVarietyCount = 0;
|
|
if (Component->GrassTypeSummary.bInvalid)
|
|
{
|
|
// Update the component grass type summary
|
|
ComponentMaxGrassCullDistance = 0.0;
|
|
for (ULandscapeGrassType* GrassType : Component->GetGrassTypes())
|
|
{
|
|
if (GrassType != nullptr)
|
|
{
|
|
for (FGrassVariety& GrassVariety : GrassType->GrassVarieties)
|
|
{
|
|
if (GrassVariety.GrassMesh != nullptr)
|
|
{
|
|
int32 EndCullDistance = bGrassQualityLevelEnabled ? GrassVariety.EndCullDistanceQuality.GetValue(GGrassQualityLevel) : GrassVariety.EndCullDistance.GetValue();
|
|
ComponentMaxGrassCullDistance = FMath::Max(ComponentMaxGrassCullDistance, EndCullDistance);
|
|
ComponentGrassVarietyCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Component->GrassTypeSummary.MaxInstanceDiscardDistance = ComponentMaxGrassCullDistance;
|
|
Component->GrassTypeSummary.bHasAnyGrass = (ComponentGrassVarietyCount > 0);
|
|
Component->GrassTypeSummary.bInvalid = false;
|
|
}
|
|
else
|
|
{
|
|
ComponentMaxGrassCullDistance = Component->GrassTypeSummary.MaxInstanceDiscardDistance;
|
|
}
|
|
|
|
bProxyHasAnyGrass = bProxyHasAnyGrass || Component->GrassTypeSummary.bHasAnyGrass;
|
|
ProxyMaxGrassCullDistance = FMath::Max(ProxyMaxGrassCullDistance, ComponentMaxGrassCullDistance);
|
|
}
|
|
|
|
GrassTypeSummary.bHasAnyGrass = bProxyHasAnyGrass;
|
|
GrassTypeSummary.MaxInstanceDiscardDistance = ProxyMaxGrassCullDistance;
|
|
GrassTypeSummary.LandscapeComponentCount = LandscapeComponents.Num();
|
|
}
|
|
|
|
// the purpose of this class is to copy the lightmap from the terrain, and set the CoordinateScale and CoordinateBias to zero.
|
|
// we re-use the same texture references, so the memory cost is relatively minimal.
|
|
class FLandscapeGrassLightMap : public FLightMap2D
|
|
{
|
|
public:
|
|
FLandscapeGrassLightMap(const FLightMap2D& InLightMap)
|
|
: FLightMap2D(InLightMap)
|
|
{
|
|
CoordinateScale = FVector2D::ZeroVector;
|
|
CoordinateBias = FVector2D::ZeroVector;
|
|
}
|
|
};
|
|
|
|
// the purpose of this class is to copy the shadowmap from the terrain, and set the CoordinateScale and CoordinateBias to zero.
|
|
// we re-use the same texture references, so the memory cost is relatively minimal.
|
|
class FLandscapeGrassShadowMap : public FShadowMap2D
|
|
{
|
|
public:
|
|
FLandscapeGrassShadowMap(const FShadowMap2D& InShadowMap)
|
|
: FShadowMap2D(InShadowMap)
|
|
{
|
|
CoordinateScale = FVector2D::ZeroVector;
|
|
CoordinateBias = FVector2D::ZeroVector;
|
|
}
|
|
};
|
|
|
|
//
|
|
// UMaterialExpressionLandscapeGrassOutput
|
|
//
|
|
FName UMaterialExpressionLandscapeGrassOutput::PinDefaultName = TEXT("Input");
|
|
|
|
UMaterialExpressionLandscapeGrassOutput::UMaterialExpressionLandscapeGrassOutput(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
// Structure to hold one-time initialization
|
|
struct FConstructorStatics
|
|
{
|
|
FText STRING_Landscape;
|
|
FName NAME_Grass;
|
|
FConstructorStatics()
|
|
: STRING_Landscape(LOCTEXT("Landscape", "Landscape"))
|
|
, NAME_Grass("Grass")
|
|
{
|
|
}
|
|
};
|
|
static FConstructorStatics ConstructorStatics;
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
MenuCategories.Add(ConstructorStatics.STRING_Landscape);
|
|
|
|
// No outputs
|
|
Outputs.Reset();
|
|
#endif
|
|
|
|
// Default input
|
|
new(GrassTypes)FGrassInput(ConstructorStatics.NAME_Grass);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
int32 UMaterialExpressionLandscapeGrassOutput::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
|
|
{
|
|
if (GrassTypes.IsValidIndex(OutputIndex))
|
|
{
|
|
// Default input to 0 if not connected.
|
|
int32 CodeInput = GrassTypes[OutputIndex].Input.IsConnected() ? GrassTypes[OutputIndex].Input.Compile(Compiler) : Compiler->Constant(0.f);
|
|
return Compiler->CustomOutput(this, OutputIndex, CodeInput);
|
|
}
|
|
|
|
return INDEX_NONE;
|
|
}
|
|
|
|
void UMaterialExpressionLandscapeGrassOutput::GetCaption(TArray<FString>& OutCaptions) const
|
|
{
|
|
OutCaptions.Add(TEXT("Landscape Grass"));
|
|
}
|
|
|
|
TArrayView<FExpressionInput*> UMaterialExpressionLandscapeGrassOutput::GetInputsView()
|
|
{
|
|
CachedInputs.Empty();
|
|
CachedInputs.Reserve(GrassTypes.Num());
|
|
for (auto& GrassType : GrassTypes)
|
|
{
|
|
CachedInputs.Add(&GrassType.Input);
|
|
}
|
|
return CachedInputs;
|
|
}
|
|
|
|
FExpressionInput* UMaterialExpressionLandscapeGrassOutput::GetInput(int32 InputIndex)
|
|
{
|
|
return GrassTypes.IsValidIndex(InputIndex) ? &GrassTypes[InputIndex].Input : nullptr;
|
|
}
|
|
|
|
FName UMaterialExpressionLandscapeGrassOutput::GetInputName(int32 InputIndex) const
|
|
{
|
|
return GrassTypes[InputIndex].Name;
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
|
|
#if WITH_EDITOR
|
|
void UMaterialExpressionLandscapeGrassOutput::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
|
{
|
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
|
|
|
if (PropertyChangedEvent.MemberProperty)
|
|
{
|
|
const FName PropertyName = PropertyChangedEvent.MemberProperty->GetFName();
|
|
if (PropertyName == GET_MEMBER_NAME_CHECKED(UMaterialExpressionLandscapeGrassOutput, GrassTypes))
|
|
{
|
|
for (FGrassInput& Input : GrassTypes)
|
|
{
|
|
ValidateInputName(Input);
|
|
}
|
|
|
|
if (GraphNode)
|
|
{
|
|
GraphNode->ReconstructNode();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UMaterialExpressionLandscapeGrassOutput::ValidateInputName(FGrassInput& InInput) const
|
|
{
|
|
if (Material != nullptr)
|
|
{
|
|
int32 NameIndex = 1;
|
|
bool bFoundValidName = false;
|
|
|
|
// Parameters cannot be named Name_None, use the default name instead
|
|
FName PotentialName = (InInput.Name == NAME_None) ? UMaterialExpressionLandscapeGrassOutput::PinDefaultName : InInput.Name;
|
|
|
|
// Find an available unique name
|
|
while (!bFoundValidName)
|
|
{
|
|
if (NameIndex != 1)
|
|
{
|
|
PotentialName.SetNumber(NameIndex);
|
|
}
|
|
|
|
bFoundValidName = true;
|
|
|
|
// Make sure the name is unique among others pins of this node
|
|
for (const FGrassInput& OtherInput : GrassTypes)
|
|
{
|
|
if (&OtherInput != &InInput)
|
|
{
|
|
if (OtherInput.Name == PotentialName)
|
|
{
|
|
bFoundValidName = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
++NameIndex;
|
|
}
|
|
|
|
InInput.Name = PotentialName;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
//
|
|
// FGrassVariety
|
|
//
|
|
|
|
FGrassVariety::FGrassVariety()
|
|
: GrassMesh(nullptr)
|
|
, GrassDensity(400)
|
|
, GrassDensityQuality(400.0f)
|
|
, bUseGrid(true)
|
|
, PlacementJitter(1.0f)
|
|
, StartCullDistance(10000)
|
|
, StartCullDistanceQuality(10000)
|
|
, EndCullDistance(10000)
|
|
, EndCullDistanceQuality(10000)
|
|
, MinLOD(-1)
|
|
, AllowedDensityRange(0.0f, 1.0f)
|
|
, Scaling(EGrassScaling::Uniform)
|
|
, ScaleX(1.0f, 1.0f)
|
|
, ScaleY(1.0f, 1.0f)
|
|
, ScaleZ(1.0f, 1.0f)
|
|
, RandomRotation(true)
|
|
, AlignToSurface(true)
|
|
, bUseLandscapeLightmap(false)
|
|
, bReceivesDecals(true)
|
|
, bAffectDistanceFieldLighting(false)
|
|
, bCastDynamicShadow(true)
|
|
, bCastContactShadow(true)
|
|
, bKeepInstanceBufferCPUCopy(false)
|
|
, InstanceWorldPositionOffsetDisableDistance(0)
|
|
, ShadowCacheInvalidationBehavior(EShadowCacheInvalidationBehavior::Auto)
|
|
{
|
|
GrassDensityQuality.SetQualityLevelCVarForCooking(GGrassQualityLevelCVarName, GGrassQualityLevelScalabilitySection);
|
|
StartCullDistanceQuality.SetQualityLevelCVarForCooking(GGrassQualityLevelCVarName, GGrassQualityLevelScalabilitySection);
|
|
EndCullDistanceQuality.SetQualityLevelCVarForCooking(GGrassQualityLevelCVarName, GGrassQualityLevelScalabilitySection);
|
|
}
|
|
|
|
bool FGrassVariety::IsGrassQualityLevelEnable() const
|
|
{
|
|
return (GEngine && GEngine->UseGrassVarityPerQualityLevels);
|
|
}
|
|
|
|
int32 FGrassVariety::GetStartCullDistance() const
|
|
{
|
|
if (IsGrassQualityLevelEnable())
|
|
{
|
|
return StartCullDistanceQuality.GetValue(GGrassQualityLevel);
|
|
}
|
|
else
|
|
{
|
|
return StartCullDistance.GetValue();
|
|
}
|
|
}
|
|
|
|
int32 FGrassVariety::GetEndCullDistance() const
|
|
{
|
|
if (IsGrassQualityLevelEnable())
|
|
{
|
|
return EndCullDistanceQuality.GetValue(GGrassQualityLevel);
|
|
}
|
|
else
|
|
{
|
|
return EndCullDistance.GetValue();
|
|
}
|
|
}
|
|
|
|
float FGrassVariety::GetDensity() const
|
|
{
|
|
if (IsGrassQualityLevelEnable())
|
|
{
|
|
return GrassDensityQuality.GetValue(GGrassQualityLevel);
|
|
}
|
|
else
|
|
{
|
|
return GrassDensity.GetValue();
|
|
}
|
|
}
|
|
|
|
|
|
// ULandscapeGrassType
|
|
//
|
|
|
|
ULandscapeGrassType::ULandscapeGrassType(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
GrassDensity_DEPRECATED = 400;
|
|
StartCullDistance_DEPRECATED = 10000;
|
|
EndCullDistance_DEPRECATED = 10000;
|
|
PlacementJitter_DEPRECATED = 1.0f;
|
|
RandomRotation_DEPRECATED = true;
|
|
AlignToSurface_DEPRECATED = true;
|
|
bEnableDensityScaling = true;
|
|
}
|
|
|
|
void ULandscapeGrassType::PostLoad()
|
|
{
|
|
Super::PostLoad();
|
|
if (GrassMesh_DEPRECATED && !GrassVarieties.Num())
|
|
{
|
|
FGrassVariety Grass;
|
|
Grass.GrassMesh = GrassMesh_DEPRECATED;
|
|
Grass.GrassDensity = GrassDensity_DEPRECATED;
|
|
Grass.StartCullDistance = StartCullDistance_DEPRECATED;
|
|
Grass.EndCullDistance = EndCullDistance_DEPRECATED;
|
|
Grass.PlacementJitter = PlacementJitter_DEPRECATED;
|
|
Grass.RandomRotation = RandomRotation_DEPRECATED;
|
|
Grass.AlignToSurface = AlignToSurface_DEPRECATED;
|
|
|
|
GrassVarieties.Add(Grass);
|
|
GrassMesh_DEPRECATED = nullptr;
|
|
}
|
|
|
|
StateHash = ComputeStateHash();
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void ULandscapeGrassType::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
|
{
|
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
|
|
|
// Look for landscape components using this grass type, and clear their grass type caches
|
|
if (GIsEditor)
|
|
{
|
|
for (TObjectIterator<ALandscapeProxy> It(/*AdditionalExclusionFlags = */RF_ClassDefaultObject, /*bIncludeDerivedClasses = */true, /*InInternalExclusionFlags = */EInternalObjectFlags::Garbage); It; ++It)
|
|
{
|
|
ALandscapeProxy* Proxy = *It;
|
|
if (Proxy->GetWorld() && !Proxy->GetWorld()->IsPlayInEditor())
|
|
{
|
|
for (ULandscapeComponent* Component : Proxy->LandscapeComponents)
|
|
{
|
|
if (Component->GetGrassTypes().Contains(this))
|
|
{
|
|
Component->InvalidateGrassTypeSummary();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the state hash (so we detect the change and rebuild the relevant grass)
|
|
StateHash = ComputeStateHash();
|
|
}
|
|
#endif
|
|
|
|
uint32 ULandscapeGrassType::ComputeStateHash()
|
|
{
|
|
uint32 Hash = 0;
|
|
|
|
FArchiveCrc32 Ar;
|
|
|
|
// Save and nullify previous Hash value (don't want to include it in the new calculated hash)
|
|
uint32 PreviousHash = StateHash;
|
|
StateHash = 0;
|
|
|
|
Serialize(Ar);
|
|
Hash = Ar.GetCrc();
|
|
|
|
// Restore previous hash value
|
|
StateHash = PreviousHash;
|
|
|
|
return Hash;
|
|
}
|
|
|
|
//
|
|
// FLandscapeComponentGrassData
|
|
//
|
|
SIZE_T FLandscapeComponentGrassData::GetAllocatedSize() const
|
|
{
|
|
return sizeof(*this) + HeightWeightData.GetAllocatedSize() + WeightOffsets.GetAllocatedSize();
|
|
}
|
|
|
|
bool FLandscapeComponentGrassData::HasWeightData() const
|
|
{
|
|
return !!WeightOffsets.Num();
|
|
}
|
|
|
|
bool FLandscapeComponentGrassData::HasValidData() const
|
|
{
|
|
// If NumElements < 0, its data wasn't computed.
|
|
// If == 0, its data was computed (i.e. is valid), but removed (for saving space) because its weight data is all zero :
|
|
// If > 0, its data was computed and contains non-zero weight data
|
|
return (NumElements >= 0);
|
|
}
|
|
|
|
bool FLandscapeComponentGrassData::HasData() const
|
|
{
|
|
if (HasValidData())
|
|
{
|
|
int32 LocalNumElements = HeightWeightData.Num();
|
|
#if WITH_EDITORONLY_DATA
|
|
if (LocalNumElements == 0)
|
|
{
|
|
LocalNumElements = HeightMipData.Num();
|
|
}
|
|
#endif
|
|
return LocalNumElements > 0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
TArrayView<uint8> FLandscapeComponentGrassData::GetWeightData(const ULandscapeGrassType* GrassType)
|
|
{
|
|
if (!HeightWeightData.IsEmpty())
|
|
{
|
|
if (int32* OffsetPtr = WeightOffsets.Find(GrassType))
|
|
{
|
|
int32 Offset = *OffsetPtr;
|
|
check(Offset + NumElements <= HeightWeightData.Num());
|
|
check(NumElements);
|
|
return MakeArrayView<uint8>(&HeightWeightData[Offset], NumElements);
|
|
}
|
|
}
|
|
|
|
return TArrayView<uint8>();
|
|
}
|
|
|
|
bool FLandscapeComponentGrassData::Contains(ULandscapeGrassType* GrassType) const
|
|
{
|
|
return WeightOffsets.Contains(GrassType);
|
|
}
|
|
|
|
TArrayView<uint16> FLandscapeComponentGrassData::GetHeightData()
|
|
{
|
|
if (HeightWeightData.IsEmpty())
|
|
{
|
|
return TArrayView<uint16>();
|
|
}
|
|
|
|
check(NumElements <= HeightWeightData.Num());
|
|
return MakeArrayView<uint16>((uint16*)&HeightWeightData[0], NumElements);
|
|
}
|
|
|
|
void FLandscapeComponentGrassData::InitializeFrom(const TArray<uint16>& HeightData, const TMap<ULandscapeGrassType*, TArray<uint8>>& WeightData)
|
|
{
|
|
WeightOffsets.Empty(WeightData.Num());
|
|
|
|
// If weight data is empty make sure we don't have any memory allocated to grass
|
|
if (WeightData.Num() == 0)
|
|
{
|
|
NumElements = 0;
|
|
HeightWeightData.Empty();
|
|
return;
|
|
}
|
|
|
|
NumElements = HeightData.Num();
|
|
HeightWeightData.SetNumUninitialized(NumElements * sizeof(uint16) + NumElements * WeightData.Num() * sizeof(uint8));
|
|
|
|
uint8* CopyDest = HeightWeightData.GetData();
|
|
int32 CopyOffset = 0;
|
|
int32 CopySize = HeightData.Num() * sizeof(uint16);
|
|
|
|
check((CopyOffset + CopySize) <= HeightWeightData.Num());
|
|
|
|
FMemory::Memcpy(&CopyDest[CopyOffset], HeightData.GetData(), CopySize);
|
|
|
|
CopyOffset += CopySize;
|
|
CopySize = NumElements * sizeof(uint8);
|
|
|
|
for (const TPair<ULandscapeGrassType*,TArray<uint8>>& Pair : WeightData)
|
|
{
|
|
WeightOffsets.Add(Pair.Key, CopyOffset);
|
|
check(Pair.Value.Num() == NumElements);
|
|
check((CopyOffset + CopySize) <= HeightWeightData.Num());
|
|
|
|
FMemory::Memcpy(&CopyDest[CopyOffset], Pair.Value.GetData(), CopySize);
|
|
CopyOffset += CopySize;
|
|
}
|
|
}
|
|
|
|
// Note that this is a relatively expensive operation (in the runtime sense), as it's making a copy of the buffers
|
|
void FLandscapeComponentGrassData::InitializeFrom(IBuffer2DView<uint16>* HeightData, TMap<ULandscapeGrassType*, IBuffer2DView<uint8>*>& WeightData, bool bStripEmptyWeights)
|
|
{
|
|
const int32 OriginalGrassTypeCount = WeightData.Num();
|
|
WeightOffsets.Empty(OriginalGrassTypeCount);
|
|
|
|
// If weight data is empty make sure we don't have any memory allocated to grass
|
|
if (OriginalGrassTypeCount == 0)
|
|
{
|
|
NumElements = 0;
|
|
HeightWeightData.Empty();
|
|
return;
|
|
}
|
|
|
|
NumElements = HeightData->Num();
|
|
|
|
// reserve space for up to OriginalGrassTypeCount weight elements
|
|
HeightWeightData.SetNumUninitialized(NumElements * sizeof(uint16) + NumElements * OriginalGrassTypeCount * sizeof(uint8));
|
|
|
|
uint8* CopyDest = HeightWeightData.GetData();
|
|
int32 CopyOffset = 0;
|
|
int32 CopySize = NumElements * sizeof(uint16);
|
|
|
|
check((CopyOffset + CopySize) <= HeightWeightData.Num());
|
|
|
|
// copy the height data
|
|
HeightData->CopyTo((uint16*)&CopyDest[CopyOffset], CopySize / sizeof(uint16));
|
|
CopyOffset += CopySize;
|
|
CopySize = NumElements * sizeof(uint8);
|
|
|
|
for (const TPair<ULandscapeGrassType*, IBuffer2DView<uint8>*>& Pair : WeightData)
|
|
{
|
|
check(Pair.Value->Num() == NumElements);
|
|
check((CopyOffset + CopySize) <= HeightWeightData.Num());
|
|
|
|
const bool bIsAllZero = Pair.Value->CopyToAndCalcIsAllZero(&CopyDest[CopyOffset], CopySize);
|
|
|
|
if (bStripEmptyWeights && bIsAllZero)
|
|
{
|
|
// strip all zero weights
|
|
}
|
|
else
|
|
{
|
|
// record the weight data
|
|
WeightOffsets.Add(Pair.Key, CopyOffset);
|
|
CopyOffset += CopySize;
|
|
}
|
|
}
|
|
|
|
// shrink if necessary to account for stripped weight data
|
|
HeightWeightData.SetNum(CopyOffset);
|
|
}
|
|
|
|
FArchive& operator<<(FArchive& Ar, FLandscapeComponentGrassData& Data)
|
|
{
|
|
Ar.UsingCustomVersion(FLandscapeCustomVersion::GUID);
|
|
Ar.UsingCustomVersion(FFortniteMainBranchObjectVersion::GUID);
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
if (!Ar.IsFilterEditorOnly())
|
|
{
|
|
if (Ar.CustomVer(FFortniteMainBranchObjectVersion::GUID) < FFortniteMainBranchObjectVersion::LandscapeSupportPerComponentGrassTypes)
|
|
{
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
if (Ar.CustomVer(FLandscapeCustomVersion::GUID) >= FLandscapeCustomVersion::GrassMaterialInstanceFix)
|
|
{
|
|
Ar << Data.MaterialStateIds_DEPRECATED;
|
|
}
|
|
else
|
|
{
|
|
Data.MaterialStateIds_DEPRECATED.Empty(1);
|
|
if (Ar.UEVer() >= VER_UE4_SERIALIZE_LANDSCAPE_GRASS_DATA_MATERIAL_GUID)
|
|
{
|
|
FGuid MaterialStateId;
|
|
Ar << MaterialStateId;
|
|
Data.MaterialStateIds_DEPRECATED.Add(MaterialStateId);
|
|
}
|
|
}
|
|
|
|
if (Ar.CustomVer(FLandscapeCustomVersion::GUID) >= FLandscapeCustomVersion::GrassMaterialWPO)
|
|
{
|
|
Ar << Data.RotationForWPO_DEPRECATED;
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
else
|
|
{
|
|
// MaterialStateIds and RotationForWPO have been replaced with a simple hash :
|
|
Ar << Data.GenerationHash;
|
|
}
|
|
}
|
|
|
|
TArray<uint16> DeprecatedHeightData;
|
|
if (Ar.CustomVer(FFortniteMainBranchObjectVersion::GUID) < FFortniteMainBranchObjectVersion::LandscapeGrassSingleArray)
|
|
{
|
|
DeprecatedHeightData.BulkSerialize(Ar);
|
|
}
|
|
|
|
if (!Ar.IsFilterEditorOnly())
|
|
{
|
|
if (Ar.CustomVer(FLandscapeCustomVersion::GUID) >= FLandscapeCustomVersion::CollisionMaterialWPO)
|
|
{
|
|
if (Ar.CustomVer(FLandscapeCustomVersion::GUID) >= FLandscapeCustomVersion::LightmassMaterialWPO)
|
|
{
|
|
// todo - BulkSerialize each mip?
|
|
Ar << Data.HeightMipData;
|
|
}
|
|
else
|
|
{
|
|
checkSlow(Ar.IsLoading());
|
|
|
|
TArray<uint16> CollisionHeightData;
|
|
CollisionHeightData.BulkSerialize(Ar);
|
|
if (CollisionHeightData.Num())
|
|
{
|
|
const int32 ComponentSizeQuads = static_cast<int32>(FMath::Sqrt(static_cast<float>(Data.GetHeightData().Num())) - 1);
|
|
const int32 CollisionSizeQuads = static_cast<int32>(FMath::Sqrt(static_cast<float>(CollisionHeightData.Num())) - 1);
|
|
const int32 CollisionMip = FMath::FloorLog2(ComponentSizeQuads / CollisionSizeQuads);
|
|
Data.HeightMipData.Add(CollisionMip, MoveTemp(CollisionHeightData));
|
|
}
|
|
|
|
TArray<uint16> SimpleCollisionHeightData;
|
|
SimpleCollisionHeightData.BulkSerialize(Ar);
|
|
if (SimpleCollisionHeightData.Num())
|
|
{
|
|
const int32 ComponentSizeQuads = static_cast<int32>(FMath::Sqrt(static_cast<float>(Data.GetHeightData().Num())) - 1);
|
|
const int32 SimpleCollisionSizeQuads = static_cast<int32>(FMath::Sqrt(static_cast<float>(SimpleCollisionHeightData.Num())) - 1);
|
|
const int32 SimpleCollisionMip = FMath::FloorLog2(ComponentSizeQuads / SimpleCollisionSizeQuads);
|
|
Data.HeightMipData.Add(SimpleCollisionMip, MoveTemp(SimpleCollisionHeightData));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TMap<ULandscapeGrassType*, TArray<uint8>> DeprecatedWeightData;
|
|
if (Ar.CustomVer(FFortniteMainBranchObjectVersion::GUID) < FFortniteMainBranchObjectVersion::LandscapeGrassSingleArray)
|
|
{
|
|
Ar << DeprecatedWeightData;
|
|
|
|
Data.InitializeFrom(DeprecatedHeightData, DeprecatedWeightData);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
Ar << Data.NumElements;
|
|
Ar << Data.WeightOffsets;
|
|
Ar << Data.HeightWeightData;
|
|
}
|
|
|
|
return Ar;
|
|
}
|
|
|
|
void FLandscapeComponentGrassData::ConditionalDiscardDataOnLoad()
|
|
{
|
|
//check(HasValidData());
|
|
if (!GIsEditor && GGrassDiscardDataOnLoad)
|
|
{
|
|
bool bRemoved = false;
|
|
// Remove data for grass types which have scalability enabled
|
|
for (auto GrassTypeIt = WeightOffsets.CreateIterator(); GrassTypeIt; ++GrassTypeIt)
|
|
{
|
|
if (!GrassTypeIt.Key() || GrassTypeIt.Key()->bEnableDensityScaling)
|
|
{
|
|
GrassTypeIt.RemoveCurrent();
|
|
bRemoved = true;
|
|
}
|
|
}
|
|
|
|
// If all grass types have been removed, discard the height data too.
|
|
if (WeightOffsets.Num() == 0)
|
|
{
|
|
// NOTE: this is NOT thread safe, as we are overwriting the existing grassdata here
|
|
// However if it is only performed on load, then we can assume no async tasks are holding shared refs to this yet.
|
|
*this = FLandscapeComponentGrassData();
|
|
NumElements = 0;
|
|
}
|
|
else if (bRemoved)
|
|
{
|
|
TMap<ULandscapeGrassType*, int32> PreviousOffsets(ObjectPtrDecay(MoveTemp(WeightOffsets)));
|
|
TArray<uint8> PreviousHeightWeightData(MoveTemp(HeightWeightData));
|
|
HeightWeightData.SetNumUninitialized(NumElements * sizeof(uint16) + NumElements * PreviousOffsets.Num() * sizeof(uint8), EAllowShrinking::Yes);
|
|
|
|
uint8* CopyDest = HeightWeightData.GetData();
|
|
uint8* CopySrc = PreviousHeightWeightData.GetData();
|
|
int32 CopyOffset = 0;
|
|
int32 CopySize = NumElements * sizeof(uint16);
|
|
|
|
check((CopyOffset + CopySize) <= HeightWeightData.Num());
|
|
|
|
FMemory::Memcpy(&CopyDest[CopyOffset], PreviousHeightWeightData.GetData(), CopySize);
|
|
|
|
CopyOffset += CopySize;
|
|
CopySize = NumElements * sizeof(uint8);
|
|
|
|
for (const TPair<ULandscapeGrassType*, int32>& Pair : PreviousOffsets)
|
|
{
|
|
int32 PreviousOffset = Pair.Value;
|
|
WeightOffsets.Add(Pair.Key, CopyOffset);
|
|
check((CopyOffset + CopySize) <= HeightWeightData.Num());
|
|
check((PreviousOffset + CopySize) <= PreviousHeightWeightData.Num());
|
|
|
|
FMemory::Memcpy(&CopyDest[CopyOffset], &CopySrc[PreviousOffset], CopySize);
|
|
CopyOffset += CopySize;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// ALandscapeProxy grass-related functions
|
|
//
|
|
|
|
struct FGrassBuilderBase
|
|
{
|
|
bool bHaveValidData;
|
|
float GrassDensity;
|
|
FVector DrawScale;
|
|
FVector DrawLoc;
|
|
FMatrix LandscapeToWorld;
|
|
|
|
FIntPoint SectionBase;
|
|
FIntPoint LandscapeSectionOffset;
|
|
int32 ComponentSizeQuads;
|
|
FVector Origin;
|
|
FVector Extent;
|
|
FVector ComponentOrigin;
|
|
|
|
int32 SqrtMaxInstances;
|
|
|
|
FGrassBuilderBase(ALandscapeProxy* Landscape, ULandscapeComponent* Component, const FGrassVariety& GrassVariety, ERHIFeatureLevel::Type FeatureLevel, int32 SqrtSubsections = 1, int32 SubX = 0, int32 SubY = 0, bool bEnableDensityScaling = true)
|
|
{
|
|
const float DensityScale = bEnableDensityScaling ? GGrassDensityScale : 1.0f;
|
|
GrassDensity = GrassVariety.GetDensity() * DensityScale;
|
|
|
|
DrawScale = Landscape->GetRootComponent()->GetRelativeScale3D();
|
|
DrawLoc = Landscape->GetActorLocation();
|
|
LandscapeSectionOffset = Landscape->LandscapeSectionOffset;
|
|
|
|
SectionBase = Component->GetSectionBase();
|
|
ComponentSizeQuads = Component->ComponentSizeQuads;
|
|
|
|
Origin.X = DrawScale.X * SectionBase.X;
|
|
Origin.Y = DrawScale.Y * SectionBase.Y;
|
|
Origin.Z = 0.0f;
|
|
|
|
Extent.X = DrawScale.X * ComponentSizeQuads;
|
|
Extent.Y = DrawScale.Y * ComponentSizeQuads;
|
|
Extent.Z = 0.0f;
|
|
|
|
ComponentOrigin.X = DrawScale.X * (SectionBase.X - LandscapeSectionOffset.X);
|
|
ComponentOrigin.Y = DrawScale.Y * (SectionBase.Y - LandscapeSectionOffset.Y);
|
|
ComponentOrigin.Z = 0.0f;
|
|
|
|
SqrtMaxInstances = FMath::CeilToInt32(FMath::Sqrt(FMath::Abs(Extent.X * Extent.Y * GrassDensity / 1000.0f / 1000.0f)));
|
|
|
|
bHaveValidData = SqrtMaxInstances != 0;
|
|
|
|
LandscapeToWorld = Landscape->GetRootComponent()->GetComponentTransform().ToMatrixNoScale();
|
|
|
|
if (bHaveValidData && SqrtSubsections != 1)
|
|
{
|
|
check(SqrtMaxInstances > 2 * SqrtSubsections);
|
|
SqrtMaxInstances /= SqrtSubsections;
|
|
check(SqrtMaxInstances > 0);
|
|
|
|
Extent.X /= SqrtSubsections;
|
|
Extent.Y /= SqrtSubsections;
|
|
|
|
Origin.X += Extent.X * SubX;
|
|
Origin.Y += Extent.Y * SubY;
|
|
}
|
|
}
|
|
};
|
|
|
|
// FLandscapeComponentGrassAccess - accessor wrapper for data for one GrassType from one Component
|
|
struct FLandscapeComponentGrassAccess
|
|
{
|
|
FLandscapeComponentGrassAccess(const ULandscapeComponent* InComponent, const ULandscapeGrassType* GrassType)
|
|
: GrassData(InComponent->GrassData)
|
|
, HeightData(InComponent->GrassData->GetHeightData())
|
|
, WeightData(InComponent->GrassData->GetWeightData(GrassType))
|
|
, Stride(InComponent->ComponentSizeQuads + 1)
|
|
{}
|
|
|
|
bool IsValid()
|
|
{
|
|
return WeightData.Num() == FMath::Square(Stride) && HeightData.Num() == FMath::Square(Stride);
|
|
}
|
|
|
|
FORCEINLINE float GetHeight(int32 IdxX, int32 IdxY)
|
|
{
|
|
return LandscapeDataAccess::GetLocalHeight(HeightData[IdxX + Stride*IdxY]);
|
|
}
|
|
FORCEINLINE float GetWeight(int32 IdxX, int32 IdxY) // usages
|
|
{
|
|
return ((float)WeightData[IdxX + Stride*IdxY]) / 255.f;
|
|
}
|
|
|
|
FORCEINLINE int32 GetStride()
|
|
{
|
|
return Stride;
|
|
}
|
|
|
|
private:
|
|
TSharedRef<FLandscapeComponentGrassData, ESPMode::ThreadSafe> GrassData;
|
|
TArrayView<uint16> HeightData;
|
|
TArrayView<uint8> WeightData;
|
|
int32 Stride;
|
|
};
|
|
|
|
struct FAsyncGrassBuilder : public FGrassBuilderBase
|
|
{
|
|
FLandscapeComponentGrassAccess GrassData;
|
|
EGrassScaling Scaling;
|
|
FFloatInterval ScaleX;
|
|
FFloatInterval ScaleY;
|
|
FFloatInterval ScaleZ;
|
|
FFloatInterval AllowedDensityRange;
|
|
bool bWeightAttenuatesMaxScale;
|
|
float MaxScaleWeightAttenuation ;
|
|
bool bRandomRotation;
|
|
bool bRandomScale;
|
|
bool bAlignToSurface;
|
|
float PlacementJitter;
|
|
FRandomStream RandomStream;
|
|
FMatrix XForm;
|
|
FBox MeshBox;
|
|
int32 DesiredInstancesPerLeaf;
|
|
|
|
double BuildTime;
|
|
int32 TotalInstances;
|
|
uint32 HaltonBaseIndex;
|
|
|
|
bool bUseLandscapeLightmap;
|
|
FVector2D LightmapBaseBias;
|
|
FVector2D LightmapBaseScale;
|
|
FVector2D ShadowmapBaseBias;
|
|
FVector2D ShadowmapBaseScale;
|
|
FVector2D LightMapComponentBias;
|
|
FVector2D LightMapComponentScale;
|
|
bool bRequiresCPUAccess;
|
|
|
|
TArray<FBox> ExcludedBoxes;
|
|
|
|
// output
|
|
FStaticMeshInstanceData InstanceBuffer;
|
|
TArray<FClusterNode> ClusterTree;
|
|
int32 OutOcclusionLayerNum;
|
|
|
|
FAsyncGrassBuilder(ALandscapeProxy* Landscape, ULandscapeComponent* Component, const ULandscapeGrassType* GrassType, const FGrassVariety& GrassVariety, ERHIFeatureLevel::Type FeatureLevel, UGrassInstancedStaticMeshComponent* GrassInstancedStaticMeshComponent, int32 SqrtSubsections, int32 SubX, int32 SubY, uint32 InHaltonBaseIndex, TArray<FBox>& InExcludedBoxes)
|
|
: FGrassBuilderBase(Landscape, Component, GrassVariety, FeatureLevel, SqrtSubsections, SubX, SubY, GrassType->bEnableDensityScaling)
|
|
, GrassData(Component, GrassType)
|
|
, Scaling(GrassVariety.Scaling)
|
|
, ScaleX(GrassVariety.ScaleX)
|
|
, ScaleY(GrassVariety.ScaleY)
|
|
, ScaleZ(GrassVariety.ScaleZ)
|
|
, AllowedDensityRange(GrassVariety.AllowedDensityRange)
|
|
, bWeightAttenuatesMaxScale(GrassVariety.bWeightAttenuatesMaxScale)
|
|
, MaxScaleWeightAttenuation(GrassVariety.MaxScaleWeightAttenuation)
|
|
, bRandomRotation(GrassVariety.RandomRotation)
|
|
, bRandomScale(false)
|
|
, bAlignToSurface(GrassVariety.AlignToSurface)
|
|
, PlacementJitter(GrassVariety.PlacementJitter)
|
|
, RandomStream(GrassInstancedStaticMeshComponent->InstancingRandomSeed)
|
|
, XForm(LandscapeToWorld * GrassInstancedStaticMeshComponent->GetComponentTransform().ToMatrixWithScale().Inverse())
|
|
, MeshBox(GrassVariety.GrassMesh->GetBounds().GetBox())
|
|
, DesiredInstancesPerLeaf(GrassInstancedStaticMeshComponent->DesiredInstancesPerLeaf())
|
|
|
|
, BuildTime(0)
|
|
, TotalInstances(0)
|
|
, HaltonBaseIndex(InHaltonBaseIndex)
|
|
|
|
, bUseLandscapeLightmap(GrassVariety.bUseLandscapeLightmap)
|
|
, LightmapBaseBias(FVector2D::ZeroVector)
|
|
, LightmapBaseScale(FVector2D::UnitVector)
|
|
, ShadowmapBaseBias(FVector2D::ZeroVector)
|
|
, ShadowmapBaseScale(FVector2D::UnitVector)
|
|
, LightMapComponentBias(FVector2D::ZeroVector)
|
|
, LightMapComponentScale(FVector2D::UnitVector)
|
|
, bRequiresCPUAccess(GrassVariety.bKeepInstanceBufferCPUCopy)
|
|
// output
|
|
, InstanceBuffer(/*bSupportsVertexHalfFloat*/ true)
|
|
, ClusterTree()
|
|
, OutOcclusionLayerNum(0)
|
|
{
|
|
|
|
switch (Scaling)
|
|
{
|
|
case EGrassScaling::Uniform:
|
|
bRandomScale = ScaleX.Size() > 0;
|
|
break;
|
|
case EGrassScaling::Free:
|
|
bRandomScale = ScaleX.Size() > 0 || ScaleY.Size() > 0 || ScaleZ.Size() > 0;
|
|
break;
|
|
case EGrassScaling::LockXY:
|
|
bRandomScale = ScaleX.Size() > 0 || ScaleZ.Size() > 0;
|
|
break;
|
|
default:
|
|
check(0);
|
|
}
|
|
|
|
if (InExcludedBoxes.Num())
|
|
{
|
|
ExcludedBoxes.Reserve(InExcludedBoxes.Num());
|
|
FMatrix BoxXForm = GrassInstancedStaticMeshComponent->GetComponentToWorld().ToMatrixWithScale().Inverse() * XForm.Inverse();
|
|
for (const FBox& Box : InExcludedBoxes)
|
|
{
|
|
ExcludedBoxes.Add(Box.TransformBy(BoxXForm));
|
|
}
|
|
}
|
|
|
|
bHaveValidData = bHaveValidData && GrassData.IsValid();
|
|
|
|
InstanceBuffer.SetAllowCPUAccess(bRequiresCPUAccess);
|
|
|
|
check(DesiredInstancesPerLeaf > 0);
|
|
|
|
if (bUseLandscapeLightmap)
|
|
{
|
|
InitLandscapeLightmap(Component);
|
|
}
|
|
}
|
|
|
|
void InitLandscapeLightmap(ULandscapeComponent* Component)
|
|
{
|
|
const int32 SubsectionSizeQuads = Component->SubsectionSizeQuads;
|
|
const int32 NumSubsections = Component->NumSubsections;
|
|
const int32 LandscapeComponentSizeQuads = Component->ComponentSizeQuads;
|
|
|
|
const int32 StaticLightingLOD = Component->GetLandscapeProxy()->StaticLightingLOD;
|
|
const int32 ComponentSizeVerts = LandscapeComponentSizeQuads + 1;
|
|
const float LightMapRes = Component->StaticLightingResolution > 0.0f ? Component->StaticLightingResolution : Component->GetLandscapeProxy()->StaticLightingResolution;
|
|
const int32 LightingLOD = Component->GetLandscapeProxy()->StaticLightingLOD;
|
|
|
|
// Calculate mapping from landscape to lightmap space for mapping landscape grass to the landscape lightmap
|
|
// Copied from the calculation of FLandscapeUniformShaderParameters::LandscapeLightmapScaleBias in FLandscapeComponentSceneProxy::OnTransformChanged()
|
|
int32 PatchExpandCountX = 0;
|
|
int32 PatchExpandCountY = 0;
|
|
int32 DesiredSize = 1;
|
|
const float LightMapRatio = ::GetTerrainExpandPatchCount(LightMapRes, PatchExpandCountX, PatchExpandCountY, LandscapeComponentSizeQuads, (NumSubsections * (SubsectionSizeQuads + 1)), DesiredSize, LightingLOD);
|
|
const float LightmapLODScaleX = LightMapRatio / ((ComponentSizeVerts >> StaticLightingLOD) + 2 * PatchExpandCountX);
|
|
const float LightmapLODScaleY = LightMapRatio / ((ComponentSizeVerts >> StaticLightingLOD) + 2 * PatchExpandCountY);
|
|
const float LightmapBiasX = PatchExpandCountX * LightmapLODScaleX;
|
|
const float LightmapBiasY = PatchExpandCountY * LightmapLODScaleY;
|
|
const float LightmapScaleX = LightmapLODScaleX * (float)((ComponentSizeVerts >> StaticLightingLOD) - 1) / LandscapeComponentSizeQuads;
|
|
const float LightmapScaleY = LightmapLODScaleY * (float)((ComponentSizeVerts >> StaticLightingLOD) - 1) / LandscapeComponentSizeQuads;
|
|
|
|
LightMapComponentScale = FVector2D(LightmapScaleX, LightmapScaleY) / FVector2D(DrawScale);
|
|
LightMapComponentBias = FVector2D(LightmapBiasX, LightmapBiasY);
|
|
|
|
const FMeshMapBuildData* MeshMapBuildData = Component->GetMeshMapBuildData();
|
|
|
|
if (MeshMapBuildData != nullptr)
|
|
{
|
|
if (MeshMapBuildData->LightMap.IsValid())
|
|
{
|
|
LightmapBaseBias = MeshMapBuildData->LightMap->GetLightMap2D()->GetCoordinateBias();
|
|
LightmapBaseScale = MeshMapBuildData->LightMap->GetLightMap2D()->GetCoordinateScale();
|
|
}
|
|
|
|
if (MeshMapBuildData->ShadowMap.IsValid())
|
|
{
|
|
ShadowmapBaseBias = MeshMapBuildData->ShadowMap->GetShadowMap2D()->GetCoordinateBias();
|
|
ShadowmapBaseScale = MeshMapBuildData->ShadowMap->GetShadowMap2D()->GetCoordinateScale();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetInstance(int32 InstanceIndex, const FMatrix& InXForm, float RandomFraction)
|
|
{
|
|
if (bUseLandscapeLightmap)
|
|
{
|
|
FMatrix::FReal InstanceX = InXForm.M[3][0];
|
|
FMatrix::FReal InstanceY = InXForm.M[3][1];
|
|
|
|
FVector2D NormalizedGrassCoordinate;
|
|
NormalizedGrassCoordinate.X = (InstanceX - ComponentOrigin.X) * LightMapComponentScale.X + LightMapComponentBias.X;
|
|
NormalizedGrassCoordinate.Y = (InstanceY - ComponentOrigin.Y) * LightMapComponentScale.Y + LightMapComponentBias.Y;
|
|
|
|
FVector2D LightMapCoordinate = NormalizedGrassCoordinate * LightmapBaseScale + LightmapBaseBias;
|
|
FVector2D ShadowMapCoordinate = NormalizedGrassCoordinate * ShadowmapBaseScale + ShadowmapBaseBias;
|
|
|
|
InstanceBuffer.SetInstance(InstanceIndex, FMatrix44f(InXForm), RandomStream.GetFraction(), LightMapCoordinate, ShadowMapCoordinate);
|
|
}
|
|
else
|
|
{
|
|
InstanceBuffer.SetInstance(InstanceIndex, FMatrix44f(InXForm), RandomStream.GetFraction());
|
|
}
|
|
}
|
|
|
|
FVector GetDefaultScale() const
|
|
{
|
|
FVector Result( ScaleX.Min > 0.0f && FMath::IsNearlyZero(ScaleX.Size()) ? ScaleX.Min : 1.0f,
|
|
ScaleY.Min > 0.0f && FMath::IsNearlyZero(ScaleY.Size()) ? ScaleY.Min : 1.0f,
|
|
ScaleZ.Min > 0.0f && FMath::IsNearlyZero(ScaleZ.Size()) ? ScaleZ.Min : 1.0f);
|
|
switch (Scaling)
|
|
{
|
|
case EGrassScaling::Uniform:
|
|
Result.Y = Result.X;
|
|
Result.Z = Result.X;
|
|
break;
|
|
case EGrassScaling::Free:
|
|
break;
|
|
case EGrassScaling::LockXY:
|
|
Result.Y = Result.X;
|
|
break;
|
|
default:
|
|
check(0);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
FVector GetRandomScale(float InWeight) const
|
|
{
|
|
FVector Result(1.0f);
|
|
|
|
float WeightAttenuationFactor = 1.0f;
|
|
if (bWeightAttenuatesMaxScale)
|
|
{
|
|
WeightAttenuationFactor = MaxScaleWeightAttenuation < 1.0f ? FMath::Clamp((InWeight - MaxScaleWeightAttenuation) / (1.0f - MaxScaleWeightAttenuation), 0.0f, 1.0f) : 0.0f;
|
|
}
|
|
|
|
switch (Scaling)
|
|
{
|
|
case EGrassScaling::Uniform:
|
|
Result.X = ScaleX.Interpolate(RandomStream.GetFraction() * WeightAttenuationFactor);
|
|
Result.Y = Result.X;
|
|
Result.Z = Result.X;
|
|
break;
|
|
case EGrassScaling::Free:
|
|
Result.X = ScaleX.Interpolate(RandomStream.GetFraction() * WeightAttenuationFactor);
|
|
Result.Y = ScaleY.Interpolate(RandomStream.GetFraction() * WeightAttenuationFactor);
|
|
Result.Z = ScaleZ.Interpolate(RandomStream.GetFraction() * WeightAttenuationFactor);
|
|
break;
|
|
case EGrassScaling::LockXY:
|
|
Result.X = ScaleX.Interpolate(RandomStream.GetFraction() * WeightAttenuationFactor);
|
|
Result.Y = Result.X;
|
|
Result.Z = ScaleZ.Interpolate(RandomStream.GetFraction() * WeightAttenuationFactor);
|
|
break;
|
|
default:
|
|
check(0);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
bool IsExcluded(const FVector& LocationWithHeight)
|
|
{
|
|
for (const FBox& Box : ExcludedBoxes)
|
|
{
|
|
if (Box.IsInside(LocationWithHeight))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Build()
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassAsyncBuildTime);
|
|
check(bHaveValidData);
|
|
double StartTime = FPlatformTime::Seconds();
|
|
const FVector DefaultScale = GetDefaultScale();
|
|
float Div = 1.0f / float(SqrtMaxInstances);
|
|
TArray<FMatrix> InstanceTransforms;
|
|
if (HaltonBaseIndex)
|
|
{
|
|
if (Extent.X < 0)
|
|
{
|
|
Origin.X += Extent.X;
|
|
Extent.X *= -1.0f;
|
|
}
|
|
if (Extent.Y < 0)
|
|
{
|
|
Origin.Y += Extent.Y;
|
|
Extent.Y *= -1.0f;
|
|
}
|
|
int32 MaxNum = SqrtMaxInstances * SqrtMaxInstances;
|
|
InstanceTransforms.Reserve(MaxNum);
|
|
FVector DivExtent(Extent * Div);
|
|
for (int32 InstanceIndex = 0; InstanceIndex < MaxNum; InstanceIndex++)
|
|
{
|
|
float HaltonX = Halton(InstanceIndex + HaltonBaseIndex, 2);
|
|
float HaltonY = Halton(InstanceIndex + HaltonBaseIndex, 3);
|
|
FVector Location(Origin.X + HaltonX * Extent.X, Origin.Y + HaltonY * Extent.Y, 0.0f);
|
|
FVector LocationWithHeight;
|
|
FVector ComputedNormal;
|
|
float Weight = 0.f;
|
|
SampleLandscapeAtLocationLocal(Location, LocationWithHeight, Weight, bAlignToSurface ? &ComputedNormal : nullptr);
|
|
bool bKeep = (Weight > AllowedDensityRange.Min) && (Weight <= AllowedDensityRange.Max)
|
|
&& (Weight >= RandomStream.GetFraction())
|
|
&& !IsExcluded(LocationWithHeight);
|
|
if (bKeep)
|
|
{
|
|
const FVector Scale = bRandomScale ? GetRandomScale(Weight) : DefaultScale;
|
|
const float Rot = bRandomRotation ? RandomStream.GetFraction() * 360.0f : 0.0f;
|
|
const FMatrix BaseXForm = FScaleRotationTranslationMatrix(Scale, FRotator(0.0f, Rot, 0.0f), FVector::ZeroVector);
|
|
FMatrix OutXForm;
|
|
if (bAlignToSurface && !ComputedNormal.IsNearlyZero())
|
|
{
|
|
const FVector NewZ = ComputedNormal * FMath::Sign(ComputedNormal.Z);
|
|
const FVector NewX = (FVector(0, -1, 0) ^ NewZ).GetSafeNormal();
|
|
const FVector NewY = NewZ ^ NewX;
|
|
const FMatrix Align = FMatrix(NewX, NewY, NewZ, FVector::ZeroVector);
|
|
OutXForm = (BaseXForm * Align).ConcatTranslation(LocationWithHeight) * XForm;
|
|
}
|
|
else
|
|
{
|
|
OutXForm = BaseXForm.ConcatTranslation(LocationWithHeight) * XForm;
|
|
}
|
|
InstanceTransforms.Add(OutXForm);
|
|
}
|
|
}
|
|
if (InstanceTransforms.Num())
|
|
{
|
|
TotalInstances += InstanceTransforms.Num();
|
|
InstanceBuffer.AllocateInstances(InstanceTransforms.Num(), 0, EResizeBufferFlags::AllowSlackOnGrow | EResizeBufferFlags::AllowSlackOnReduce, true);
|
|
for (int32 InstanceIndex = 0; InstanceIndex < InstanceTransforms.Num(); InstanceIndex++)
|
|
{
|
|
const FMatrix& OutXForm = InstanceTransforms[InstanceIndex];
|
|
SetInstance(InstanceIndex, OutXForm, RandomStream.GetFraction());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int32 NumKept = 0;
|
|
float MaxJitter1D = FMath::Clamp<float>(PlacementJitter, 0.0f, .99f) * Div * .5f;
|
|
FVector MaxJitter(MaxJitter1D, MaxJitter1D, 0.0f);
|
|
MaxJitter *= Extent;
|
|
Origin += Extent * (Div * 0.5f);
|
|
struct FInstanceLocal
|
|
{
|
|
FVector Pos;
|
|
bool bKeep;
|
|
float Weight;
|
|
};
|
|
TArray<FInstanceLocal> Instances;
|
|
Instances.AddUninitialized(SqrtMaxInstances * SqrtMaxInstances);
|
|
{
|
|
int32 InstanceIndex = 0;
|
|
for (int32 xStart = 0; xStart < SqrtMaxInstances; xStart++)
|
|
{
|
|
for (int32 yStart = 0; yStart < SqrtMaxInstances; yStart++)
|
|
{
|
|
FVector Location(Origin.X + float(xStart) * Div * Extent.X, Origin.Y + float(yStart) * Div * Extent.Y, 0.0f);
|
|
|
|
// NOTE: We evaluate the random numbers on the stack and store them in locals rather than inline within the FVector() constructor below, because
|
|
// the order of evaluation of function arguments in C++ is unspecified. We really want this to behave consistently on all sorts of
|
|
// different platforms!
|
|
const float FirstRandom = RandomStream.GetFraction();
|
|
const float SecondRandom = RandomStream.GetFraction();
|
|
Location += FVector(FirstRandom * 2.0f - 1.0f, SecondRandom * 2.0f - 1.0f, 0.0f) * MaxJitter;
|
|
|
|
FInstanceLocal& Instance = Instances[InstanceIndex];
|
|
float Weight = 0.f;
|
|
SampleLandscapeAtLocationLocal(Location, Instance.Pos, Weight);
|
|
Instance.bKeep = (Weight > AllowedDensityRange.Min) && (Weight <= AllowedDensityRange.Max)
|
|
&& (Weight >= RandomStream.GetFraction())
|
|
&& !IsExcluded(Instance.Pos);
|
|
Instance.Weight = Weight;
|
|
if (Instance.bKeep)
|
|
{
|
|
NumKept++;
|
|
}
|
|
InstanceIndex++;
|
|
}
|
|
}
|
|
}
|
|
if (NumKept)
|
|
{
|
|
InstanceTransforms.AddUninitialized(NumKept);
|
|
TotalInstances += NumKept;
|
|
{
|
|
InstanceBuffer.AllocateInstances(NumKept, 0, EResizeBufferFlags::AllowSlackOnGrow | EResizeBufferFlags::AllowSlackOnReduce, true);
|
|
int32 InstanceIndex = 0;
|
|
int32 OutInstanceIndex = 0;
|
|
for (int32 xStart = 0; xStart < SqrtMaxInstances; xStart++)
|
|
{
|
|
for (int32 yStart = 0; yStart < SqrtMaxInstances; yStart++)
|
|
{
|
|
const FInstanceLocal& Instance = Instances[InstanceIndex];
|
|
if (Instance.bKeep)
|
|
{
|
|
const FVector Scale = bRandomScale ? GetRandomScale(Instance.Weight) : DefaultScale;
|
|
const float Rot = bRandomRotation ? RandomStream.GetFraction() * 360.0f : 0.0f;
|
|
const FMatrix BaseXForm = FScaleRotationTranslationMatrix(Scale, FRotator(0.0f, Rot, 0.0f), FVector::ZeroVector);
|
|
FMatrix OutXForm;
|
|
if (bAlignToSurface)
|
|
{
|
|
FVector PosX1 = xStart ? Instances[InstanceIndex - SqrtMaxInstances].Pos : Instance.Pos;
|
|
FVector PosX2 = (xStart + 1 < SqrtMaxInstances) ? Instances[InstanceIndex + SqrtMaxInstances].Pos : Instance.Pos;
|
|
FVector PosY1 = yStart ? Instances[InstanceIndex - 1].Pos : Instance.Pos;
|
|
FVector PosY2 = (yStart + 1 < SqrtMaxInstances) ? Instances[InstanceIndex + 1].Pos : Instance.Pos;
|
|
|
|
if (PosX1 != PosX2 && PosY1 != PosY2)
|
|
{
|
|
FVector NewZ = ((PosX1 - PosX2) ^ (PosY1 - PosY2)).GetSafeNormal();
|
|
NewZ *= FMath::Sign(NewZ.Z);
|
|
|
|
const FVector NewX = (FVector(0, -1, 0) ^ NewZ).GetSafeNormal();
|
|
const FVector NewY = NewZ ^ NewX;
|
|
|
|
FMatrix Align = FMatrix(NewX, NewY, NewZ, FVector::ZeroVector);
|
|
OutXForm = (BaseXForm * Align).ConcatTranslation(Instance.Pos) * XForm;
|
|
}
|
|
else
|
|
{
|
|
OutXForm = BaseXForm.ConcatTranslation(Instance.Pos) * XForm;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OutXForm = BaseXForm.ConcatTranslation(Instance.Pos) * XForm;
|
|
}
|
|
InstanceTransforms[OutInstanceIndex] = OutXForm;
|
|
SetInstance(OutInstanceIndex++, OutXForm, RandomStream.GetFraction());
|
|
}
|
|
InstanceIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 NumInstances = InstanceTransforms.Num();
|
|
if (NumInstances)
|
|
{
|
|
TArray<int32> SortedInstances;
|
|
TArray<int32> InstanceReorderTable;
|
|
TArray<float> InstanceCustomDataDummy;
|
|
UGrassInstancedStaticMeshComponent::BuildTreeAnyThread(InstanceTransforms, InstanceCustomDataDummy, 0, MeshBox, ClusterTree, SortedInstances, InstanceReorderTable, OutOcclusionLayerNum, DesiredInstancesPerLeaf, false);
|
|
|
|
// in-place sort the instances and generate the sorted instance data
|
|
for (int32 FirstUnfixedIndex = 0; FirstUnfixedIndex < NumInstances; FirstUnfixedIndex++)
|
|
{
|
|
int32 LoadFrom = SortedInstances[FirstUnfixedIndex];
|
|
|
|
if (LoadFrom != FirstUnfixedIndex)
|
|
{
|
|
check(LoadFrom > FirstUnfixedIndex);
|
|
InstanceBuffer.SwapInstance(FirstUnfixedIndex, LoadFrom);
|
|
|
|
int32 SwapGoesTo = InstanceReorderTable[FirstUnfixedIndex];
|
|
check(SwapGoesTo > FirstUnfixedIndex);
|
|
check(SortedInstances[SwapGoesTo] == FirstUnfixedIndex);
|
|
SortedInstances[SwapGoesTo] = LoadFrom;
|
|
InstanceReorderTable[LoadFrom] = SwapGoesTo;
|
|
|
|
InstanceReorderTable[FirstUnfixedIndex] = FirstUnfixedIndex;
|
|
SortedInstances[FirstUnfixedIndex] = FirstUnfixedIndex;
|
|
}
|
|
}
|
|
}
|
|
BuildTime = FPlatformTime::Seconds() - StartTime;
|
|
}
|
|
|
|
FORCEINLINE_DEBUGGABLE void SampleLandscapeAtLocationLocal(const FVector& InLocation, FVector& OutLocation, float& OutLayerWeight, FVector* OutNormal = nullptr)
|
|
{
|
|
// Find location
|
|
const float TestX = static_cast<float>(InLocation.X / DrawScale.X - SectionBase.X);
|
|
const float TestY = static_cast<float>(InLocation.Y / DrawScale.Y - SectionBase.Y);
|
|
|
|
// Find data
|
|
const int32 X1 = FMath::FloorToInt(TestX);
|
|
const int32 Y1 = FMath::FloorToInt(TestY);
|
|
const int32 X2 = FMath::CeilToInt(TestX);
|
|
const int32 Y2 = FMath::CeilToInt(TestY);
|
|
|
|
// Clamp to prevent the sampling of the final columns from overflowing
|
|
const int32 IdxX1 = FMath::Clamp<int32>(X1, 0, GrassData.GetStride() - 1);
|
|
const int32 IdxY1 = FMath::Clamp<int32>(Y1, 0, GrassData.GetStride() - 1);
|
|
const int32 IdxX2 = FMath::Clamp<int32>(X2, 0, GrassData.GetStride() - 1);
|
|
const int32 IdxY2 = FMath::Clamp<int32>(Y2, 0, GrassData.GetStride() - 1);
|
|
|
|
const float LerpX = FMath::Fractional(TestX);
|
|
const float LerpY = FMath::Fractional(TestY);
|
|
|
|
// Bilinear interpolate sampled weights
|
|
const float SampleWeight11 = GrassData.GetWeight(IdxX1, IdxY1);
|
|
const float SampleWeight21 = GrassData.GetWeight(IdxX2, IdxY1);
|
|
const float SampleWeight12 = GrassData.GetWeight(IdxX1, IdxY2);
|
|
const float SampleWeight22 = GrassData.GetWeight(IdxX2, IdxY2);
|
|
|
|
OutLayerWeight = FMath::Lerp(
|
|
FMath::Lerp(SampleWeight11, SampleWeight21, LerpX),
|
|
FMath::Lerp(SampleWeight12, SampleWeight22, LerpX),
|
|
LerpY);
|
|
|
|
// Sample quad corner heights
|
|
const float SampleHeight11 = GrassData.GetHeight(IdxX1, IdxY1);
|
|
const float SampleHeight21 = GrassData.GetHeight(IdxX2, IdxY1);
|
|
const float SampleHeight12 = GrassData.GetHeight(IdxX1, IdxY2);
|
|
const float SampleHeight22 = GrassData.GetHeight(IdxX2, IdxY2);
|
|
|
|
OutLocation.X = InLocation.X - DrawScale.X * float(LandscapeSectionOffset.X);
|
|
OutLocation.Y = InLocation.Y - DrawScale.Y * float(LandscapeSectionOffset.Y);
|
|
|
|
// Compute height using triangle barycentric coordinates to account for quad triangulation
|
|
if (LerpX < LerpY)
|
|
{
|
|
double A = (1.0 - LerpY);
|
|
double B = LerpX;
|
|
double C = 1.0 - A - B;
|
|
OutLocation.Z = DrawScale.Z * (SampleHeight11 * A + SampleHeight22 * B + SampleHeight12 * C);
|
|
}
|
|
else
|
|
{
|
|
double A = (1.0 - LerpX);
|
|
double B = LerpX - LerpY;
|
|
double C = 1.0 - A - B;
|
|
OutLocation.Z = DrawScale.Z * (SampleHeight11 * A + SampleHeight21 * B + SampleHeight22 * C);
|
|
}
|
|
|
|
// Compute normal
|
|
if (OutNormal)
|
|
{
|
|
const FVector P11((float)X1, (float)Y1, SampleHeight11);
|
|
const FVector P22((float)X2, (float)Y2, SampleHeight22);
|
|
|
|
// Choose triangle and compute normal
|
|
if (LerpX > LerpY)
|
|
{
|
|
const FVector P21((float)X2, (float)Y1, SampleHeight21);
|
|
*OutNormal = (P11 != P21 && P22 != P21) ? FVector(((P22 - P21) ^ (P11 - P21)).GetSafeNormal()) : FVector::ZeroVector;
|
|
}
|
|
else
|
|
{
|
|
const FVector P12((float)X1, (float)Y2, SampleHeight12);
|
|
*OutNormal = (P11 != P12 && P22 != P12) ? FVector(((P11 - P12) ^ (P22 - P12)).GetSafeNormal()) : FVector::ZeroVector;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
void ALandscapeProxy::FlushGrassComponents(const TSet<ULandscapeComponent*>* OnlyForComponents, bool bFlushGrassMaps)
|
|
{
|
|
if (OnlyForComponents)
|
|
{
|
|
for (FCachedLandscapeFoliage::TGrassSet::TIterator Iter(FoliageCache.CachedGrassComps); Iter; ++Iter)
|
|
{
|
|
ULandscapeComponent* Component = (*Iter).Key.BasedOn.Get();
|
|
// if the weak pointer in the cache is invalid, we should kill them anyway
|
|
if (Component == nullptr || OnlyForComponents->Contains(Component))
|
|
{
|
|
UHierarchicalInstancedStaticMeshComponent *Used = (*Iter).Foliage.Get();
|
|
if (Used)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassDestoryComp);
|
|
Used->ClearInstances();
|
|
Used->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepRelative, false));
|
|
Used->DestroyComponent();
|
|
FoliageComponents.RemoveSwap(Used);
|
|
}
|
|
Iter.RemoveCurrent();
|
|
}
|
|
}
|
|
|
|
if (bFlushGrassMaps)
|
|
{
|
|
for (ULandscapeComponent* Component : *OnlyForComponents)
|
|
{
|
|
Component->RemoveGrassMap();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Clear old foliage component containers
|
|
FoliageComponents.Empty();
|
|
|
|
// Might as well clear the cache...
|
|
FoliageCache.ClearCache();
|
|
// Destroy any owned foliage components
|
|
TInlineComponentArray<UHierarchicalInstancedStaticMeshComponent*> FoliageComps;
|
|
GetComponents(FoliageComps);
|
|
for (UHierarchicalInstancedStaticMeshComponent* Component : FoliageComps)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassDestoryComp);
|
|
Component->ClearInstances();
|
|
Component->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepRelative, false));
|
|
Component->DestroyComponent();
|
|
}
|
|
|
|
check(RootComponent != nullptr);
|
|
TArray<USceneComponent*> AttachedFoliageComponents = RootComponent->GetAttachChildren().FilterByPredicate(
|
|
[](USceneComponent* Component)
|
|
{
|
|
return Cast<UHierarchicalInstancedStaticMeshComponent>(Component);
|
|
});
|
|
|
|
// Destroy any attached but un-owned foliage components
|
|
for (USceneComponent* Component : AttachedFoliageComponents)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassDestoryComp);
|
|
CastChecked<UHierarchicalInstancedStaticMeshComponent>(Component)->ClearInstances();
|
|
Component->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepRelative, false));
|
|
Component->DestroyComponent();
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
UWorld* World = GetWorld();
|
|
|
|
if (bFlushGrassMaps)
|
|
{
|
|
// Clear GrassMaps
|
|
for (UActorComponent* Component : GetComponents())
|
|
{
|
|
if (ULandscapeComponent* LandscapeComp = Cast<ULandscapeComponent>(Component))
|
|
{
|
|
LandscapeComp->RemoveGrassMap();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static uint32 GGrassExclusionChangeTag = 1;
|
|
static TMap<FWeakObjectPtr, FBox, FDefaultSetAllocator, TWeakObjectPtrMapKeyFuncs<FWeakObjectPtr, FBox>> GGrassExclusionBoxes;
|
|
|
|
void ALandscapeProxy::AddExclusionBox(FWeakObjectPtr Owner, const FBox& Box)
|
|
{
|
|
INC_DWORD_STAT(STAT_GrassExclusionVolumes);
|
|
|
|
GGrassExclusionBoxes.Add(Owner, Box);
|
|
GGrassExclusionChangeTag++;
|
|
}
|
|
void ALandscapeProxy::RemoveExclusionBox(FWeakObjectPtr Owner)
|
|
{
|
|
DEC_DWORD_STAT(STAT_GrassExclusionVolumes);
|
|
|
|
GGrassExclusionBoxes.Remove(Owner);
|
|
GGrassExclusionChangeTag++;
|
|
}
|
|
void ALandscapeProxy::RemoveAllExclusionBoxes()
|
|
{
|
|
SET_DWORD_STAT(STAT_GrassExclusionVolumes, 0);
|
|
|
|
if (GGrassExclusionBoxes.Num())
|
|
{
|
|
GGrassExclusionBoxes.Empty();
|
|
GGrassExclusionChangeTag++;
|
|
}
|
|
}
|
|
|
|
void ALandscapeProxy::RemoveInvalidExclusionBoxes()
|
|
{
|
|
for (auto Iter = GGrassExclusionBoxes.CreateIterator(); Iter; ++Iter)
|
|
{
|
|
if (!Iter->Key.IsValid())
|
|
{
|
|
DEC_DWORD_STAT(STAT_GrassExclusionVolumes);
|
|
|
|
Iter.RemoveCurrent();
|
|
GGrassExclusionChangeTag++;
|
|
}
|
|
}
|
|
|
|
GGrassExclusionBoxes.Compact();
|
|
}
|
|
|
|
void ALandscapeProxy::DebugDrawExclusionBoxes(const UWorld* InWorld)
|
|
{
|
|
#if !UE_BUILD_SHIPPING
|
|
if (bGGrassDrawExclusionVolumes)
|
|
{
|
|
for (auto Iter = GGrassExclusionBoxes.CreateIterator(); Iter; ++Iter)
|
|
{
|
|
if (Iter->Key.IsValid())
|
|
{
|
|
const FBox& Box = Iter->Value;
|
|
DrawDebugBox(InWorld, Box.GetCenter(), Box.GetExtent(), FColor::Red);
|
|
}
|
|
}
|
|
}
|
|
#endif // !UE_BUILD_SHIPPING
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
|
|
void ALandscapeProxy::BuildGrassMaps()
|
|
{
|
|
check(!HasAnyFlags(RF_ClassDefaultObject));
|
|
|
|
// Update all grass types
|
|
for (ULandscapeComponent* Component : LandscapeComponents)
|
|
{
|
|
if (Component->UpdateGrassTypes())
|
|
{
|
|
Component->MarkPackageDirty();
|
|
}
|
|
}
|
|
UpdateGrassTypeSummary();
|
|
|
|
// now ensure that all of our components have up-to-date grass maps
|
|
ULandscapeSubsystem *Subsystem = GetWorld()->GetSubsystem<ULandscapeSubsystem>();
|
|
if (Subsystem)
|
|
{
|
|
const bool bMarkDirty = true;
|
|
Subsystem->GetGrassMapBuilder()->BuildGrassMapsNowForComponents(LandscapeComponents, /*InSlowTask = */nullptr, bMarkDirty);
|
|
}
|
|
}
|
|
|
|
|
|
int32 ALandscapeProxy::GetOutdatedGrassMapCount() const
|
|
{
|
|
int32 GrassMapsNeedingRender = 0;
|
|
if (GGrassEnable > 0)
|
|
{
|
|
GrassMapsNeedingRender = GetWorld()->GetSubsystem<ULandscapeSubsystem>()->GetGrassMapBuilder()->CountOutdatedGrassMaps(LandscapeComponents);
|
|
}
|
|
return GrassMapsNeedingRender;
|
|
}
|
|
|
|
#endif
|
|
|
|
// amortized update path (unless bForceSync is true)
|
|
void ALandscapeProxy::UpdateGrass(const TArray<FVector>& Cameras, bool bForceSync /* = false */)
|
|
{
|
|
int32 InOutNumCompsCreated = 0;
|
|
UpdateGrass(Cameras, InOutNumCompsCreated, bForceSync);
|
|
}
|
|
|
|
// amortized update path (unless bForceSync is true)
|
|
void ALandscapeProxy::UpdateGrass(const TArray<FVector>& Cameras, int32& InOutNumCompsCreated, bool bForceSync /* = false */)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_GrassUpdate);
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ALandscapeProxy::UpdateGrass);
|
|
|
|
// don't let it create grass actors inside a transaction:
|
|
if (GUndo != nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UWorld* World = GetWorld();
|
|
ULandscapeSubsystem* LandscapeSubsystem = World ? World->GetSubsystem<ULandscapeSubsystem>() : nullptr;
|
|
const bool bIsGrassCreationPrioritized = LandscapeSubsystem ? LandscapeSubsystem->IsGrassCreationPrioritized() : false;
|
|
const bool bWaitAsyncTasks = bForceSync || bIsGrassCreationPrioritized;
|
|
|
|
if (GGrassEnable > 0)
|
|
{
|
|
float GuardBand = GGuardBandMultiplier;
|
|
float DiscardGuardBand = GGuardBandDiscardMultiplier;
|
|
bool bCullSubsections = GCullSubsections > 0;
|
|
bool bDisableGPUCull = GDisableGPUCull > 0;
|
|
bool bDisableDynamicShadows = GDisableDynamicShadows > 0;
|
|
int32 MaxInstancesPerComponent = FMath::Max<int32>(1024, GMaxInstancesPerComponent);
|
|
int32 MaxTasks = GMaxAsyncTasks;
|
|
int32 GrassMaxCreatePerFrame = GGrassMaxCreatePerFrame;
|
|
if (bIsGrassCreationPrioritized && GGrassCreationPrioritizedMultipler > 1)
|
|
{
|
|
MaxTasks *= GGrassCreationPrioritizedMultipler;
|
|
GrassMaxCreatePerFrame *= GGrassCreationPrioritizedMultipler;
|
|
}
|
|
|
|
const float CullDistanceScale = GGrassCullDistanceScale;
|
|
|
|
if (World)
|
|
{
|
|
// Cull grass max distance based on Cull Distance scale factor and on max GuardBand factor.
|
|
// Note this is the proxy combined distance, not the per-component distance...
|
|
float GrassCullDistanceScalar = GGrassCullDistanceScale * FMath::Max(GGuardBandDiscardMultiplier, GGuardBandMultiplier);
|
|
float ProxyGrassMaxCulledDiscardDistance = GrassTypeSummary.MaxInstanceDiscardDistance * GrassCullDistanceScalar;
|
|
float ProxyGrassMaxSquareDiscardDistance = ProxyGrassMaxCulledDiscardDistance * ProxyGrassMaxCulledDiscardDistance;
|
|
|
|
ERHIFeatureLevel::Type FeatureLevel = World->Scene->GetFeatureLevel();
|
|
|
|
struct SortedLandscapeElement
|
|
{
|
|
SortedLandscapeElement(ULandscapeComponent* InComponent, float InMinDistance, const FBox& InBoundsBox)
|
|
: Component(InComponent)
|
|
, MinDistance(InMinDistance)
|
|
, BoundsBox(InBoundsBox)
|
|
{}
|
|
|
|
ULandscapeComponent* Component;
|
|
float MinDistance;
|
|
FBox BoundsBox;
|
|
};
|
|
|
|
static TArray<SortedLandscapeElement> SortedLandscapeComponents;
|
|
SortedLandscapeComponents.Reset(LandscapeComponents.Num());
|
|
|
|
for (ULandscapeComponent* Component : LandscapeComponents)
|
|
{
|
|
// skip if we have no data and no way to generate it
|
|
if (Component == nullptr || !Component->GrassData->HasValidData() || !Component->GrassData->HasWeightData())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FBoxSphereBounds WorldBounds = Component->CalcBounds(Component->GetComponentTransform());
|
|
float MinSqrDistanceToComponent = Cameras.Num() ? MAX_flt : 0.0f;
|
|
for (const FVector& CameraPos : Cameras)
|
|
{
|
|
MinSqrDistanceToComponent = FMath::Min<float>(MinSqrDistanceToComponent, static_cast<float>(WorldBounds.ComputeSquaredDistanceFromBoxToPoint(CameraPos)));
|
|
}
|
|
|
|
if ((ProxyGrassMaxSquareDiscardDistance > 0.f) && (MinSqrDistanceToComponent > ProxyGrassMaxSquareDiscardDistance))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SortedLandscapeComponents.Emplace(Component, FMath::Sqrt(MinSqrDistanceToComponent), WorldBounds.GetBox());
|
|
}
|
|
|
|
// Prioritize components that are closer to camera when grass creation is prioritized
|
|
bool bShouldSort = bIsGrassCreationPrioritized;
|
|
#if WITH_EDITOR
|
|
// Also when editing landscape (for a more reactive update)
|
|
bShouldSort = bShouldSort || GLandscapeEditModeActive;
|
|
#endif
|
|
if (bShouldSort)
|
|
{
|
|
Algo::Sort(SortedLandscapeComponents, [](const SortedLandscapeElement& A, const SortedLandscapeElement& B) { return A.MinDistance < B.MinDistance; });
|
|
}
|
|
|
|
// for every component that passes above, we iterate all grass types, grass varieties, and subsections (culling along the way)
|
|
// every subsection gets looked up in the cache and touched (to indicate it is still valid)
|
|
// if the subsection doesn't exist we try to start creating it (hence why we sorted the list in priority order)
|
|
for (const SortedLandscapeElement& SortedLandscapeComponent : SortedLandscapeComponents)
|
|
{
|
|
ULandscapeComponent* Component = SortedLandscapeComponent.Component;
|
|
float MinDistanceToComp = SortedLandscapeComponent.MinDistance;
|
|
|
|
// update the component's grass exclusion boxes if necessary
|
|
if (Component->ChangeTag != GGrassExclusionChangeTag)
|
|
{
|
|
Component->ActiveExcludedBoxes.Empty();
|
|
if (GGrassExclusionBoxes.Num() && GIgnoreExcludeBoxes == 0)
|
|
{
|
|
const FBox& WorldBox = SortedLandscapeComponent.BoundsBox;
|
|
|
|
for (const TPair<FWeakObjectPtr, FBox>& Pair : GGrassExclusionBoxes)
|
|
{
|
|
if (Pair.Value.Intersect(WorldBox))
|
|
{
|
|
if (Pair.Key.IsValid())
|
|
{
|
|
// this will also filter out boxes that are completely inside of the other boxes
|
|
Component->ActiveExcludedBoxes.AddUnique(ULandscapeComponent::FExcludeBox(Pair.Value));
|
|
}
|
|
}
|
|
}
|
|
|
|
Component->ActiveExcludedBoxes.Shrink();
|
|
|
|
//Sort exclude boxes by their volume size, as the biggest boxes are more likely to exclude points and early out when building grass
|
|
Component->ActiveExcludedBoxes.Sort([](const ULandscapeComponent::FExcludeBox& A, const ULandscapeComponent::FExcludeBox& B) {
|
|
return A.Box.GetVolume() > B.Box.GetVolume();
|
|
}
|
|
);
|
|
}
|
|
Component->ChangeTag = GGrassExclusionChangeTag;
|
|
}
|
|
|
|
const TArray<ULandscapeGrassType*>& LandscapeGrassTypes = Component->GetGrassTypes();
|
|
for (ULandscapeGrassType* GrassType : LandscapeGrassTypes)
|
|
{
|
|
if (GrassType)
|
|
{
|
|
if (World->IsGameWorld() && !Component->GrassData->Contains(GrassType)) // TODO [chris.tchou] why do we only do this in game worlds?
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int32 GrassVarietyIndex = -1;
|
|
uint32 HaltonBaseIndex = 1;
|
|
for (auto& GrassVariety : GrassType->GrassVarieties)
|
|
{
|
|
GrassVarietyIndex++;
|
|
int32 EndCullDistance = GrassVariety.GetEndCullDistance();
|
|
if (GrassVariety.GrassMesh && GrassVariety.GetDensity() > 0.0f && EndCullDistance > 0)
|
|
{
|
|
float MustHaveDistance = GuardBand * (float)EndCullDistance * CullDistanceScale;
|
|
float DiscardDistance = DiscardGuardBand * (float)EndCullDistance * CullDistanceScale;
|
|
|
|
bool bUseHalton = !GrassVariety.bUseGrid;
|
|
|
|
if (!bUseHalton && MinDistanceToComp > DiscardDistance)
|
|
{
|
|
continue; // we can early out if we're not using halton sequence (otherwise we have to at least update the HaltonBaseIndex)
|
|
}
|
|
|
|
FGrassBuilderBase ForSubsectionMath(this, Component, GrassVariety, FeatureLevel);
|
|
int32 SqrtSubsections = 1;
|
|
if (ForSubsectionMath.bHaveValidData && ForSubsectionMath.SqrtMaxInstances > 0)
|
|
{
|
|
SqrtSubsections = FMath::Clamp<int32>(FMath::CeilToInt(float(ForSubsectionMath.SqrtMaxInstances) / FMath::Sqrt((float)MaxInstancesPerComponent)), 1, 16);
|
|
}
|
|
int32 MaxInstancesSub = FMath::Square(ForSubsectionMath.SqrtMaxInstances / SqrtSubsections);
|
|
|
|
if (bUseHalton && MinDistanceToComp > DiscardDistance)
|
|
{
|
|
HaltonBaseIndex += MaxInstancesSub * SqrtSubsections * SqrtSubsections;
|
|
continue; // now that we have updated the halton base index, it's ok to skip the rest
|
|
}
|
|
|
|
FBox LocalBox = Component->CachedLocalBox;
|
|
FVector LocalExtentDiv = (LocalBox.Max - LocalBox.Min) * FVector(1.0f / float(SqrtSubsections), 1.0f / float(SqrtSubsections), 1.0f);
|
|
for (int32 SubX = 0; SubX < SqrtSubsections; SubX++)
|
|
{
|
|
for (int32 SubY = 0; SubY < SqrtSubsections; SubY++)
|
|
{
|
|
float MinDistanceToSubComp = MinDistanceToComp;
|
|
|
|
FBox WorldSubBox;
|
|
|
|
if ((bCullSubsections && SqrtSubsections > 1) || Component->ActiveExcludedBoxes.Num())
|
|
{
|
|
FVector BoxMin;
|
|
BoxMin.X = LocalBox.Min.X + LocalExtentDiv.X * float(SubX);
|
|
BoxMin.Y = LocalBox.Min.Y + LocalExtentDiv.Y * float(SubY);
|
|
BoxMin.Z = LocalBox.Min.Z;
|
|
|
|
FVector BoxMax;
|
|
BoxMax.X = LocalBox.Min.X + LocalExtentDiv.X * float(SubX + 1);
|
|
BoxMax.Y = LocalBox.Min.Y + LocalExtentDiv.Y * float(SubY + 1);
|
|
BoxMax.Z = LocalBox.Max.Z;
|
|
|
|
FBox LocalSubBox(BoxMin, BoxMax);
|
|
WorldSubBox = LocalSubBox.TransformBy(Component->GetComponentTransform());
|
|
|
|
if (bCullSubsections && SqrtSubsections > 1)
|
|
{
|
|
MinDistanceToSubComp = Cameras.Num() ? MAX_flt : 0.0f;
|
|
for (auto& Pos : Cameras)
|
|
{
|
|
MinDistanceToSubComp = FMath::Min<float>(MinDistanceToSubComp, static_cast<float>(ComputeSquaredDistanceFromBoxToPoint(WorldSubBox.Min, WorldSubBox.Max, Pos)));
|
|
}
|
|
MinDistanceToSubComp = FMath::Sqrt(MinDistanceToSubComp);
|
|
}
|
|
}
|
|
|
|
if (bUseHalton)
|
|
{
|
|
HaltonBaseIndex += MaxInstancesSub; // we are going to pre-increment this for all of the continues...however we need to subtract later if we actually do this sub
|
|
}
|
|
|
|
if (MinDistanceToSubComp > DiscardDistance)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FCachedLandscapeFoliage::FGrassComp NewComp;
|
|
NewComp.Key.BasedOn = Component;
|
|
NewComp.Key.GrassType = GrassType;
|
|
NewComp.Key.SqrtSubsections = SqrtSubsections;
|
|
NewComp.Key.CachedMaxInstancesPerComponent = MaxInstancesPerComponent;
|
|
NewComp.Key.SubsectionX = SubX;
|
|
NewComp.Key.SubsectionY = SubY;
|
|
NewComp.Key.NumVarieties = GrassType->GrassVarieties.Num();
|
|
NewComp.Key.VarietyIndex = GrassVarietyIndex;
|
|
|
|
bool bRebuildForBoxes = false;
|
|
|
|
{
|
|
FCachedLandscapeFoliage::FGrassComp* Existing = FoliageCache.CachedGrassComps.Find(NewComp.Key);
|
|
if (Existing && !Existing->PreviousFoliage.IsValid() && Existing->ExclusionChangeTag != GGrassExclusionChangeTag && !Existing->PendingRemovalRebuild && !Existing->Pending)
|
|
{
|
|
for (const ULandscapeComponent::FExcludeBox& BoxWrapper : Component->ActiveExcludedBoxes)
|
|
{
|
|
if (BoxWrapper.Box.Intersect(WorldSubBox))
|
|
{
|
|
NewComp.ExcludedBoxes.Add(BoxWrapper.Box);
|
|
}
|
|
}
|
|
NewComp.ExcludedBoxes.Shrink();
|
|
if (NewComp.ExcludedBoxes != Existing->ExcludedBoxes)
|
|
{
|
|
bRebuildForBoxes = true;
|
|
NewComp.PreviousFoliage = Existing->Foliage;
|
|
Existing->PendingRemovalRebuild = true;
|
|
}
|
|
else
|
|
{
|
|
Existing->ExclusionChangeTag = GGrassExclusionChangeTag;
|
|
}
|
|
}
|
|
|
|
if (Existing || MinDistanceToSubComp > MustHaveDistance)
|
|
{
|
|
if (Existing)
|
|
{
|
|
Existing->Touch();
|
|
}
|
|
if (!bRebuildForBoxes)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bRebuildForBoxes && !bForceSync && (InOutNumCompsCreated >= GrassMaxCreatePerFrame || AsyncFoliageTasks.Num() >= MaxTasks))
|
|
{
|
|
continue; // one per frame, but we still want to touch the existing ones and we must do the rebuilds because we changed the tag
|
|
}
|
|
if (!bRebuildForBoxes)
|
|
{
|
|
for (const ULandscapeComponent::FExcludeBox& BoxWrapper : Component->ActiveExcludedBoxes)
|
|
{
|
|
if (BoxWrapper.Box.Intersect(WorldSubBox))
|
|
{
|
|
NewComp.ExcludedBoxes.Add(BoxWrapper.Box);
|
|
}
|
|
}
|
|
NewComp.ExcludedBoxes.Shrink();
|
|
}
|
|
NewComp.ExclusionChangeTag = GGrassExclusionChangeTag;
|
|
|
|
check(Component->GrassData->HasValidData());
|
|
|
|
InOutNumCompsCreated++;
|
|
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassStartComp);
|
|
|
|
// To guarantee consistency across platforms, we force the string to be lowercase and always treat it as an ANSI string.
|
|
int32 FolSeed = FCrc::StrCrc32( StringCast<ANSICHAR>( *FString::Printf( TEXT("%s%s%d %d %d"), *GrassType->GetName().ToLower(), *Component->GetName().ToLower(), SubX, SubY, GrassVarietyIndex)).Get() );
|
|
if (FolSeed == 0)
|
|
{
|
|
FolSeed++;
|
|
}
|
|
|
|
// Do not record the transaction of creating temp component for visualizations
|
|
ClearFlags(RF_Transactional);
|
|
bool PreviousPackageDirtyFlag = GetOutermost()->IsDirty();
|
|
|
|
UGrassInstancedStaticMeshComponent* GrassInstancedStaticMeshComponent;
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GrassCreateComp);
|
|
GrassInstancedStaticMeshComponent = NewObject<UGrassInstancedStaticMeshComponent>(this, NAME_None, RF_Transient);
|
|
}
|
|
NewComp.Foliage = GrassInstancedStaticMeshComponent;
|
|
FoliageCache.CachedGrassComps.Add(NewComp);
|
|
|
|
GrassInstancedStaticMeshComponent->Mobility = EComponentMobility::Static;
|
|
GrassInstancedStaticMeshComponent->SetStaticMesh(GrassVariety.GrassMesh);
|
|
GrassInstancedStaticMeshComponent->MinLOD = GrassVariety.MinLOD;
|
|
GrassInstancedStaticMeshComponent->bSelectable = false;
|
|
GrassInstancedStaticMeshComponent->bHasPerInstanceHitProxies = false;
|
|
GrassInstancedStaticMeshComponent->bReceivesDecals = GrassVariety.bReceivesDecals;
|
|
static FName NoCollision(TEXT("NoCollision"));
|
|
GrassInstancedStaticMeshComponent->SetCollisionProfileName(NoCollision);
|
|
GrassInstancedStaticMeshComponent->bDisableCollision = true;
|
|
GrassInstancedStaticMeshComponent->SetCanEverAffectNavigation(false);
|
|
GrassInstancedStaticMeshComponent->InstancingRandomSeed = FolSeed;
|
|
GrassInstancedStaticMeshComponent->LightingChannels = GrassVariety.LightingChannels;
|
|
GrassInstancedStaticMeshComponent->bHoldout = bHoldout;
|
|
GrassInstancedStaticMeshComponent->bCastStaticShadow = false;
|
|
GrassInstancedStaticMeshComponent->CastShadow = (GrassVariety.bCastDynamicShadow || GrassVariety.bCastContactShadow) && !bDisableDynamicShadows;
|
|
GrassInstancedStaticMeshComponent->bAffectDistanceFieldLighting = GrassVariety.bAffectDistanceFieldLighting;
|
|
GrassInstancedStaticMeshComponent->bCastDynamicShadow = GrassVariety.bCastDynamicShadow && !bDisableDynamicShadows;
|
|
GrassInstancedStaticMeshComponent->bCastContactShadow = GrassVariety.bCastContactShadow && !bDisableDynamicShadows;
|
|
GrassInstancedStaticMeshComponent->OverrideMaterials = GrassVariety.OverrideMaterials;
|
|
GrassInstancedStaticMeshComponent->bEvaluateWorldPositionOffset = true;
|
|
GrassInstancedStaticMeshComponent->WorldPositionOffsetDisableDistance = GrassVariety.InstanceWorldPositionOffsetDisableDistance;
|
|
GrassInstancedStaticMeshComponent->ShadowCacheInvalidationBehavior = GrassVariety.ShadowCacheInvalidationBehavior;
|
|
|
|
GrassInstancedStaticMeshComponent->PrecachePSOs();
|
|
|
|
const FMeshMapBuildData* MeshMapBuildData = Component->GetMeshMapBuildData();
|
|
|
|
if (GrassVariety.bUseLandscapeLightmap
|
|
&& GrassVariety.GrassMesh->GetNumLODs() > 0
|
|
&& MeshMapBuildData
|
|
&& MeshMapBuildData->LightMap)
|
|
{
|
|
GrassInstancedStaticMeshComponent->SetLODDataCount(GrassVariety.GrassMesh->GetNumLODs(), GrassVariety.GrassMesh->GetNumLODs());
|
|
|
|
FLightMapRef GrassLightMap = new FLandscapeGrassLightMap(*MeshMapBuildData->LightMap->GetLightMap2D());
|
|
FShadowMapRef GrassShadowMap = MeshMapBuildData->ShadowMap ? new FLandscapeGrassShadowMap(*MeshMapBuildData->ShadowMap->GetShadowMap2D()) : nullptr;
|
|
|
|
for (auto& LOD : GrassInstancedStaticMeshComponent->LODData)
|
|
{
|
|
// This trasient OverrideMapBuildData will be cleaned up by UMapBuildDataRegistry::CleanupTransientOverrideMapBuildData() if the underlying MeshMapBuildData is gone
|
|
LOD.OverrideMapBuildData = MakeUnique<FMeshMapBuildData>();
|
|
LOD.OverrideMapBuildData->LightMap = GrassLightMap;
|
|
LOD.OverrideMapBuildData->ShadowMap = GrassShadowMap;
|
|
LOD.OverrideMapBuildData->ResourceCluster = MeshMapBuildData->ResourceCluster;
|
|
}
|
|
}
|
|
|
|
if (!Cameras.Num() || bDisableGPUCull)
|
|
{
|
|
// if we don't have any cameras, then we are rendering landscape LOD materials or somesuch and we want to disable culling
|
|
GrassInstancedStaticMeshComponent->InstanceStartCullDistance = 0;
|
|
GrassInstancedStaticMeshComponent->InstanceEndCullDistance = 0;
|
|
}
|
|
else
|
|
{
|
|
GrassInstancedStaticMeshComponent->InstanceStartCullDistance = static_cast<int32>(GrassVariety.GetStartCullDistance() * CullDistanceScale);
|
|
GrassInstancedStaticMeshComponent->InstanceEndCullDistance = static_cast<int32>(GrassVariety.GetEndCullDistance() * CullDistanceScale);
|
|
}
|
|
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GrassAttachComp);
|
|
|
|
GrassInstancedStaticMeshComponent->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
|
|
FTransform DesiredTransform = GetRootComponent()->GetComponentTransform();
|
|
DesiredTransform.RemoveScaling();
|
|
GrassInstancedStaticMeshComponent->SetWorldTransform(DesiredTransform);
|
|
|
|
FoliageComponents.Add(GrassInstancedStaticMeshComponent);
|
|
}
|
|
|
|
FAsyncGrassBuilder* Builder;
|
|
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GrassCreateBuilder);
|
|
|
|
uint32 HaltonIndexForSub = 0;
|
|
if (bUseHalton)
|
|
{
|
|
check(HaltonBaseIndex > (uint32)MaxInstancesSub);
|
|
HaltonIndexForSub = HaltonBaseIndex - (uint32)MaxInstancesSub;
|
|
}
|
|
Builder = new FAsyncGrassBuilder(this, Component, GrassType, GrassVariety, FeatureLevel, GrassInstancedStaticMeshComponent, SqrtSubsections, SubX, SubY, HaltonIndexForSub, NewComp.ExcludedBoxes);
|
|
}
|
|
|
|
if (Builder->bHaveValidData)
|
|
{
|
|
FAsyncTask<FAsyncGrassTask>* Task = new FAsyncTask<FAsyncGrassTask>(Builder, NewComp.Key, GrassInstancedStaticMeshComponent);
|
|
|
|
Task->StartBackgroundTask();
|
|
|
|
AsyncFoliageTasks.Add(Task);
|
|
}
|
|
else
|
|
{
|
|
delete Builder;
|
|
}
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_GrassRegisterComp);
|
|
|
|
GrassInstancedStaticMeshComponent->RegisterComponent();
|
|
}
|
|
|
|
SetFlags(RF_Transactional);
|
|
GetOutermost()->SetDirtyFlag(PreviousPackageDirtyFlag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TSet<UHierarchicalInstancedStaticMeshComponent *> StillUsed;
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Grass_StillUsed);
|
|
|
|
// trim cached items based on time, pending and emptiness
|
|
double OldestToKeepTime = FPlatformTime::Seconds() - GMinTimeToKeepGrass;
|
|
uint32 OldestToKeepFrame = GFrameNumber - GMinFramesToKeepGrass * GetGrassUpdateInterval();
|
|
for (FCachedLandscapeFoliage::TGrassSet::TIterator Iter(FoliageCache.CachedGrassComps); Iter; ++Iter)
|
|
{
|
|
const FCachedLandscapeFoliage::FGrassComp& GrassItem = *Iter;
|
|
UHierarchicalInstancedStaticMeshComponent *Used = GrassItem.Foliage.Get();
|
|
UHierarchicalInstancedStaticMeshComponent *UsedPrev = GrassItem.PreviousFoliage.Get();
|
|
bool bOld =
|
|
!GrassItem.Pending &&
|
|
(
|
|
!GrassItem.Key.BasedOn.Get() ||
|
|
!GrassItem.Key.GrassType.Get() ||
|
|
!Used ||
|
|
(GrassItem.LastUsedFrameNumber < OldestToKeepFrame && GrassItem.LastUsedTime < OldestToKeepTime)
|
|
);
|
|
if (bOld)
|
|
{
|
|
Iter.RemoveCurrent();
|
|
}
|
|
else if (Used || UsedPrev)
|
|
{
|
|
if (!StillUsed.Num())
|
|
{
|
|
StillUsed.Reserve(FoliageCache.CachedGrassComps.Num());
|
|
}
|
|
if (Used)
|
|
{
|
|
StillUsed.Add(Used);
|
|
}
|
|
if (UsedPrev)
|
|
{
|
|
StillUsed.Add(UsedPrev);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove components that are no longer used from the components list
|
|
if (StillUsed.Num() < FoliageComponents.Num())
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Grass_DelComps);
|
|
|
|
// delete components that are no longer used
|
|
for (int32 Index = 0; Index < FoliageComponents.Num(); Index++)
|
|
{
|
|
UHierarchicalInstancedStaticMeshComponent* HComponent = FoliageComponents[Index];
|
|
if (!StillUsed.Contains(HComponent))
|
|
{
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassDestoryComp);
|
|
if (HComponent)
|
|
{
|
|
HComponent->ClearInstances();
|
|
HComponent->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepRelative, false));
|
|
HComponent->DestroyComponent();
|
|
}
|
|
FoliageComponents.RemoveAtSwap(Index--);
|
|
}
|
|
if (!bForceSync)
|
|
{
|
|
break; // one per frame is fine
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check if any async tasks have completed, and finalize them
|
|
ProcessAsyncGrassInstanceTasks(bWaitAsyncTasks, bForceSync, StillUsed);
|
|
}
|
|
|
|
void ALandscapeProxy::ProcessAsyncGrassInstanceTasks(bool bWaitAsyncTasks, bool bForceSync, const TSet<UHierarchicalInstancedStaticMeshComponent*>& StillUsed)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_Grass_FinishAsync);
|
|
// finish async tasks
|
|
for (int32 Index = 0; Index < AsyncFoliageTasks.Num(); Index++)
|
|
{
|
|
FAsyncTask<FAsyncGrassTask>* Task = AsyncFoliageTasks[Index];
|
|
if (bWaitAsyncTasks)
|
|
{
|
|
Task->EnsureCompletion();
|
|
}
|
|
if (Task->IsDone())
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassEndComp);
|
|
FAsyncGrassTask& Inner = Task->GetTask();
|
|
// We need to preserve the order here, otherwise we'll have new jobs that are added to the end jumping up in front of the queue and an original second job would be updated the last
|
|
AsyncFoliageTasks.RemoveAt(Index--);
|
|
UGrassInstancedStaticMeshComponent* GrassISMComponent = Cast<UGrassInstancedStaticMeshComponent>(Inner.Foliage.Get());
|
|
int32 NumBuiltRenderInstances = Inner.Builder->InstanceBuffer.GetNumInstances();
|
|
//UE_LOG(LogCore, Display, TEXT("%d instances in %4.0fms %6.0f instances / sec"), NumBuiltRenderInstances, 1000.0f * float(Inner.Builder->BuildTime), float(NumBuiltRenderInstances) / float(Inner.Builder->BuildTime));
|
|
|
|
FCachedLandscapeFoliage::FGrassCompKey& InnerKey = Inner.Key;
|
|
|
|
UE_LOG(LogGrass, Verbose, TEXT("ASYNC GRASS INSTANCES COMPLETE %s %s %d %d (%d)"),
|
|
*InnerKey.BasedOn->GetName(),
|
|
*(InnerKey.GrassType->GrassVarieties[InnerKey.VarietyIndex].GrassMesh->GetName()),
|
|
InnerKey.SubsectionX,
|
|
InnerKey.SubsectionY,
|
|
NumBuiltRenderInstances);
|
|
|
|
if (GrassISMComponent && StillUsed.Contains(GrassISMComponent))
|
|
{
|
|
if (NumBuiltRenderInstances > 0)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FoliageGrassEndComp_AcceptPrebuiltTree);
|
|
GrassISMComponent->AcceptPrebuiltTree(Inner.Builder->ClusterTree, Inner.Builder->OutOcclusionLayerNum, NumBuiltRenderInstances, &Inner.Builder->InstanceBuffer);
|
|
if (bForceSync && GetWorld())
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FoliageGrassEndComp_SyncUpdate);
|
|
GrassISMComponent->RecreateRenderState_Concurrent();
|
|
}
|
|
}
|
|
}
|
|
FCachedLandscapeFoliage::FGrassComp* Existing = FoliageCache.CachedGrassComps.Find(Inner.Key);
|
|
if (Existing)
|
|
{
|
|
Existing->Pending = false;
|
|
if (Existing->PreviousFoliage.IsValid())
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_FoliageGrassDestoryComp);
|
|
UHierarchicalInstancedStaticMeshComponent* HComponent = Existing->PreviousFoliage.Get();
|
|
if (HComponent)
|
|
{
|
|
HComponent->ClearInstances();
|
|
HComponent->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepRelative, false));
|
|
HComponent->DestroyComponent();
|
|
}
|
|
FoliageComponents.RemoveSwap(HComponent);
|
|
Existing->PreviousFoliage = nullptr;
|
|
}
|
|
|
|
Existing->Touch();
|
|
}
|
|
delete Task;
|
|
if (!bWaitAsyncTasks)
|
|
{
|
|
break; // one per frame is fine
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FAsyncGrassTask::FAsyncGrassTask(FAsyncGrassBuilder* InBuilder, const FCachedLandscapeFoliage::FGrassCompKey& InKey, UHierarchicalInstancedStaticMeshComponent* InFoliage)
|
|
: Builder(InBuilder)
|
|
, Key(InKey)
|
|
, Foliage(InFoliage)
|
|
{
|
|
}
|
|
|
|
void FAsyncGrassTask::DoWork()
|
|
{
|
|
Builder->Build();
|
|
}
|
|
|
|
FAsyncGrassTask::~FAsyncGrassTask()
|
|
{
|
|
delete Builder;
|
|
}
|
|
|
|
static void FlushGrass(const TArray<FString>& Args)
|
|
{
|
|
for (ALandscapeProxy* Landscape : TObjectRange<ALandscapeProxy>(RF_ClassDefaultObject | RF_ArchetypeObject, true, EInternalObjectFlags::Garbage))
|
|
{
|
|
Landscape->FlushGrassComponents();
|
|
}
|
|
}
|
|
|
|
static void FlushGrassPIE(const TArray<FString>& Args)
|
|
{
|
|
for (ALandscapeProxy* Landscape : TObjectRange<ALandscapeProxy>(RF_ClassDefaultObject | RF_ArchetypeObject, true, EInternalObjectFlags::Garbage))
|
|
{
|
|
Landscape->FlushGrassComponents(nullptr, false);
|
|
}
|
|
}
|
|
|
|
static void DumpExclusionBoxes(const TArray<FString>& Args)
|
|
{
|
|
for (const TPair<FWeakObjectPtr, FBox>& Pair : GGrassExclusionBoxes)
|
|
{
|
|
UObject* Owner = Pair.Key.Get();
|
|
UE_LOG(LogCore, Warning, TEXT("%f %f %f %f %f %f %s"),
|
|
Pair.Value.Min.X,
|
|
Pair.Value.Min.Y,
|
|
Pair.Value.Min.Z,
|
|
Pair.Value.Max.X,
|
|
Pair.Value.Max.Y,
|
|
Pair.Value.Max.Z,
|
|
Owner ? *Owner->GetFullName() : TEXT("[stale]")
|
|
);
|
|
}
|
|
}
|
|
|
|
static FAutoConsoleCommand FlushGrassCmd(
|
|
TEXT("grass.FlushCache"),
|
|
TEXT("Flush the grass cache, debugging."),
|
|
FConsoleCommandWithArgsDelegate::CreateStatic(&FlushGrass)
|
|
);
|
|
|
|
static FAutoConsoleCommand FlushGrassCmdPIE(
|
|
TEXT("grass.FlushCachePIE"),
|
|
TEXT("Flush the grass cache, debugging."),
|
|
FConsoleCommandWithArgsDelegate::CreateStatic(&FlushGrassPIE)
|
|
);
|
|
|
|
static FAutoConsoleCommand DumpExclusionBoxesCmd(
|
|
TEXT("grass.DumpExclusionBoxes"),
|
|
TEXT("Print the exclusion boxes, debugging."),
|
|
FConsoleCommandWithArgsDelegate::CreateStatic(&DumpExclusionBoxes)
|
|
);
|
|
|
|
|
|
#undef LOCTEXT_NAMESPACE
|