Files
UnrealEngine/Engine/Plugins/Experimental/Water/Source/Runtime/Private/WaterZoneActor.cpp
2025-05-18 13:04:45 +08:00

961 lines
34 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "WaterZoneActor.h"
#include "Engine/TextureRenderTarget2D.h"
#include "WaterModule.h"
#include "WaterSubsystem.h"
#include "WaterMeshComponent.h"
#include "WaterBodyActor.h"
#include "EngineUtils.h"
#include "LandscapeComponent.h"
#include "LandscapeProxy.h"
#include "LandscapeSubsystem.h"
#include "WaterUtils.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "WaterViewExtension.h"
#include "Algo/AnyOf.h"
#include "Engine/GameViewportClient.h"
#include "WaterBodyInfoMeshComponent.h"
#include "WaterTerrainComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(WaterZoneActor)
#if WITH_EDITOR
#include "LevelEditor.h"
#include "Modules/ModuleManager.h"
#include "WaterIconHelper.h"
#include "Components/BoxComponent.h"
#include "WaterZoneActorDesc.h"
#endif // WITH_EDITOR
static int32 ForceUpdateWaterInfoNextFrames = 0;
static FAutoConsoleVariableRef CVarForceUpdateWaterInfoNextFrames(
TEXT("r.Water.WaterInfo.ForceUpdateWaterInfoNextFrames"),
ForceUpdateWaterInfoNextFrames,
TEXT("Force the water info texture to regenerate on the next N frames. A negative value will force update every frame."));
TAutoConsoleVariable<float> CVarWaterFallbackDepth(
TEXT("r.Water.FallbackDepth"),
3000.0f,
TEXT("Depth to use for all water when there are no ground actors defined."),
ECVF_Default);
void OnSkipWaterInfoTextureRenderWhenWorldRenderingDisabled_Callback(IConsoleVariable*);
// HACK [jonathan.bard] : (details underneath)
TAutoConsoleVariable<int32> CVarSkipWaterInfoTextureRenderWhenWorldRenderingDisabled(
TEXT("r.Water.SkipWaterInfoTextureRenderWhenWorldRenderingDisabled"),
1,
TEXT("Use this to prevent the water info from rendering when world rendering is disabled."),
FConsoleVariableDelegate::CreateStatic(OnSkipWaterInfoTextureRenderWhenWorldRenderingDisabled_Callback),
ECVF_Default);
// HACK [roey.borsteinas] : This hack piggy-backs off the above hack for disabled world rendering. See details for that first.
// MRQ creates it's own View/ViewFamily when rendering which breaks compatibility with the WaterInfo RenderMethod 1.
// RenderMethod 1 tries to enqueue the water info update directly on the View but since the ULocalPlayer view is discarded before rendering, the water info texture never renders.
void OnSkipWaterInfoTextureRenderWhenWorldRenderingDisabled_Callback(IConsoleVariable*)
{
static int PreviousWaterInfoRenderMethodValue = 1;
IConsoleVariable* WaterInfoRenderMethodCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Water.WaterInfo.RenderMethod"));
if (CVarSkipWaterInfoTextureRenderWhenWorldRenderingDisabled.GetValueOnAnyThread() == 0)
{
PreviousWaterInfoRenderMethodValue = WaterInfoRenderMethodCVar->GetInt();
WaterInfoRenderMethodCVar->SetWithCurrentPriority(2);
}
else
{
WaterInfoRenderMethodCVar->SetWithCurrentPriority(PreviousWaterInfoRenderMethodValue);
}
}
AWaterZone::AWaterZone(const FObjectInitializer& Initializer)
: Super(Initializer)
, RenderTargetResolution(512, 512)
{
WaterMesh = CreateDefaultSubobject<UWaterMeshComponent>(TEXT("WaterMesh"));
SetRootComponent(WaterMesh);
ZoneExtent = FVector2D(51200., 51200.);
LocalTessellationExtent = FVector(51200., 51200., 10000.);
#if WITH_EDITOR
// Setup bounds component
{
BoundsComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("BoundsComponent"));
BoundsComponent->SetCollisionObjectType(ECC_WorldStatic);
BoundsComponent->SetCollisionResponseToAllChannels(ECR_Ignore);
BoundsComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
BoundsComponent->SetGenerateOverlapEvents(false);
BoundsComponent->SetupAttachment(WaterMesh);
// Bounds component extent is half-extent, ZoneExtent is full extent.
BoundsComponent->SetBoxExtent(FVector(ZoneExtent / 2., 8192.));
BoundsComponent->bIsEditorOnly = true;
FEngineShowFlags BoundsComponentShowFlags = BoundsComponent->GetShowFlags();
BoundsComponentShowFlags.SetVolumes(true);
BoundsComponent->SetShowFlags(BoundsComponentShowFlags);
}
if (GIsEditor && !IsTemplate())
{
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
LevelEditorModule.OnActorSelectionChanged().AddUObject(this, &AWaterZone::OnActorSelectionChanged);
}
ActorIcon = FWaterIconHelper::EnsureSpriteComponentCreated(this, TEXT("/Water/Icons/WaterZoneActorSprite"));
#endif // WITH_EDITOR
#if WITH_EDITORONLY_DATA
TessellatedWaterMeshExtent_DEPRECATED = FVector(35000., 35000., 10000.);
bIsSpatiallyLoaded = false;
#endif
}
void AWaterZone::SetZoneExtent(FVector2D NewExtent)
{
ZoneExtent = NewExtent;
OnExtentChanged();
}
FBox2D AWaterZone::GetZoneBounds2D() const
{
// GetZoneExtents returns the full extent of the zone but BoxSphereBounds expects a half-extent.
const FVector2D WaterZoneLocation = FVector2D(GetActorLocation());
const FVector2D WaterZoneHalfExtent = GetZoneExtent() / 2.0;
const FBox2D WaterZoneBounds(WaterZoneLocation - WaterZoneHalfExtent, WaterZoneLocation + WaterZoneHalfExtent);
return WaterZoneBounds;
}
void AWaterZone::GetAllDynamicWaterInfoBounds(TArray<FBox>& OutBounds) const
{
if (const FWaterViewExtension* WaterViewExtension = UWaterSubsystem::GetWaterViewExtension(GetWorld()))
{
const FWaterViewExtension::FWaterZoneInfo* WaterZoneInfo = WaterViewExtension->GetWaterZoneInfos().Find(this);
if (WaterZoneInfo != nullptr)
{
for (const FWaterViewExtension::FWaterZoneInfo::FWaterZoneViewInfo& ViewInfo : WaterZoneInfo->ViewInfos)
{
FVector Center(ViewInfo.Center);
if (!IsLocalOnlyTessellationEnabled())
{
Center = GetActorLocation();
}
const FVector HalfExtent(GetDynamicWaterInfoExtent() / 2);
OutBounds.Emplace(FBox(Center - HalfExtent, Center + HalfExtent));
}
}
else
{
UE_LOG(LogWater, Verbose, TEXT("AWaterZone (%s) GetAllDynamicWaterInfoBounds did not find any WaterZoneInfo associated to this WaterZone in FWaterViewExtension"), *GetNameSafe(this));
}
}
}
void AWaterZone::GetAllDynamicWaterInfoCenters(TArray<FVector>& OutCenters) const
{
if (const FWaterViewExtension* WaterViewExtension = UWaterSubsystem::GetWaterViewExtension(GetWorld()))
{
const FWaterViewExtension::FWaterZoneInfo* WaterZoneInfo = WaterViewExtension->GetWaterZoneInfos().Find(this);
if (WaterZoneInfo != nullptr)
{
for (const FWaterViewExtension::FWaterZoneInfo::FWaterZoneViewInfo& ViewInfo : WaterZoneInfo->ViewInfos)
{
FVector Center(ViewInfo.Center);
if (!IsLocalOnlyTessellationEnabled())
{
Center = GetActorLocation();
}
OutCenters.Emplace(Center);
}
}
else
{
UE_LOG(LogWater, Verbose, TEXT("AWaterZone (%s) GetAllDynamicWaterInfoCenters did not find any WaterZoneInfo associated to this WaterZone in FWaterViewExtension"), *GetNameSafe(this));
}
}
}
FBox AWaterZone::GetZoneBounds() const
{
const FBox2D ZoneBounds2D = GetZoneBounds2D();
// #todo_water [roey]: Water zone doesn't have an explicit z bounds yet. For now just use the x or y.
const double StreamingBoundsZ = FMath::Max(ZoneExtent.X, ZoneExtent.Y);
return FBox(FVector3d(ZoneBounds2D.Min, -StreamingBoundsZ / 2.), FVector3d(ZoneBounds2D.Max, StreamingBoundsZ / 2.0));
}
void AWaterZone::SetRenderTargetResolution(FIntPoint NewResolution)
{
RenderTargetResolution = NewResolution;
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, this);
}
void AWaterZone::BeginPlay()
{
Super::BeginPlay();
MarkForRebuild(EWaterZoneRebuildFlags::All, this);
FWorldDelegates::LevelAddedToWorld.AddUObject(this, &AWaterZone::OnLevelAddedToWorld);
FWorldDelegates::LevelRemovedFromWorld.AddUObject(this, &AWaterZone::OnLevelRemovedFromWorld);
}
void AWaterZone::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
FWorldDelegates::LevelAddedToWorld.RemoveAll(this);
FWorldDelegates::LevelRemovedFromWorld.RemoveAll(this);
Super::EndPlay(EndPlayReason);
}
void AWaterZone::PostLoadSubobjects(FObjectInstancingGraph* OuterInstanceGraph)
{
// Water mesh component was made new root component. Make sure it doesn't have a parent
WaterMesh->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
Super::PostLoadSubobjects(OuterInstanceGraph);
}
void AWaterZone::PostLoad()
{
Super::PostLoad();
#if WITH_EDITORONLY_DATA
if (GetLinkerCustomVersion(FFortniteMainBranchObjectVersion::GUID) < FFortniteMainBranchObjectVersion::WaterBodyStaticMeshComponents)
{
LocalTessellationExtent = TessellatedWaterMeshExtent_DEPRECATED;
bEnableLocalOnlyTessellation = bEnableNonTesselatedLODMesh_DEPRECATED;
}
#endif // WITH_EDITORONLY_DATA
OnExtentChanged();
}
void AWaterZone::PostRegisterAllComponents()
{
Super::PostRegisterAllComponents();
// PostRegisterAllComponents is called many times in a row during BP reinstancing, property changes, etc.
// Only register to the water body manager if we haven't already (don't have an index yet).
FWaterBodyManager* Manager = UWaterSubsystem::GetWaterBodyManager(GetWorld());
if (Manager && !IsTemplate() && (WaterZoneIndex == INDEX_NONE))
{
WaterZoneIndex = Manager->AddWaterZone(this);
}
#if WITH_EDITOR
// Register for landscape notifications
if (ULandscapeSubsystem* LandscapeSubsystem = GetWorld()->GetSubsystem<ULandscapeSubsystem>())
{
check(!OnHeightmapStreamedHandle.IsValid());
OnHeightmapStreamedHandle = LandscapeSubsystem->OnHeightmapStreamed().AddUObject(this, &AWaterZone::OnLandscapeHeightmapStreamed);
check(!OnLandscapeComponentDataChangedHandle.IsValid());
OnLandscapeComponentDataChangedHandle = LandscapeSubsystem->OnLandscapeProxyComponentDataChanged().AddUObject(this, &AWaterZone::OnLandscapeComponentDataChanged);
}
#endif // WITH_EDITOR
}
void AWaterZone::PostUnregisterAllComponents()
{
Super::PostUnregisterAllComponents();
#if WITH_EDITOR
if (ULandscapeSubsystem* LandscapeSubsystem = GetWorld()->GetSubsystem<ULandscapeSubsystem>())
{
if (OnHeightmapStreamedHandle.IsValid())
{
LandscapeSubsystem->OnHeightmapStreamed().Remove(OnHeightmapStreamedHandle);
}
if (OnLandscapeComponentDataChangedHandle.IsValid())
{
LandscapeSubsystem->OnLandscapeProxyComponentDataChanged().Remove(OnLandscapeComponentDataChangedHandle);
}
}
OnHeightmapStreamedHandle.Reset();
OnLandscapeComponentDataChangedHandle.Reset();
#endif // WITH_EDITOR
// We must check for the index because UnregisterAllComponents can be called multiple times in a row by PostEditChangeProperty, etc.
FWaterBodyManager* Manager = UWaterSubsystem::GetWaterBodyManager(GetWorld());
if (Manager && !IsTemplate() && (WaterZoneIndex != INDEX_NONE))
{
Manager->RemoveWaterZone(this);
}
WaterZoneIndex = INDEX_NONE;
}
#if WITH_EDITORONLY_DATA
void AWaterZone::DeclareConstructClasses(TArray<FTopLevelAssetPath>& OutConstructClasses, const UClass* SpecificSubclass)
{
Super::DeclareConstructClasses(OutConstructClasses, SpecificSubclass);
OutConstructClasses.Add(FTopLevelAssetPath(UBoxComponent::StaticClass()));
}
#endif
void AWaterZone::MarkForRebuild(EWaterZoneRebuildFlags Flags, const FBox2D& UpdateRegion, const UObject* DebugRequestingObject)
{
// TODO: investigate why optimizing by updating only checking the bounds of the active views dynamic extents is not working.
const FBox WaterInfoBounds = GetZoneBounds();
const FBox2D WaterInfoBounds2D(FVector2D(WaterInfoBounds.Min), FVector2D(WaterInfoBounds.Max));
// Suppress updates which occur outside the bounds of the water zone.
if ((!UpdateRegion.bIsValid || UpdateRegion.Intersect(WaterInfoBounds2D)))
{
if (EnumHasAnyFlags(Flags, EWaterZoneRebuildFlags::UpdateWaterMesh))
{
UE_LOG(LogWater, Verbose, TEXT("AWaterZone (%s) UpdateWaterMesh in region {%s} (triggered by %s)"), *GetNameSafe(this), *UpdateRegion.ToString(), *GetNameSafe(DebugRequestingObject));
WaterMesh->MarkWaterMeshGridDirty();
}
if (EnumHasAnyFlags(Flags, EWaterZoneRebuildFlags::UpdateWaterInfoTexture))
{
UE_LOG(LogWater, Verbose, TEXT("AWaterZone (%s) UpdateWaterInfoTexture in region {%s} (triggered by %s)"), *GetNameSafe(this), *UpdateRegion.ToString(), *GetNameSafe(DebugRequestingObject));
bNeedsWaterInfoRebuild = true;
}
}
}
void AWaterZone::MarkForRebuild(EWaterZoneRebuildFlags Flags, const UObject* DebugRequestingObject)
{
MarkForRebuild(Flags, FBox2D(ForceInitToZero), DebugRequestingObject);
}
void AWaterZone::ForEachWaterBodyComponent(TFunctionRef<bool(UWaterBodyComponent*)> Predicate) const
{
for (const TWeakObjectPtr<UWaterBodyComponent>& WaterBodyComponent : OwnedWaterBodies)
{
if (WaterBodyComponent.IsValid() && !Predicate(WaterBodyComponent.Get()))
{
break;
}
}
}
void AWaterZone::AddWaterBodyComponent(UWaterBodyComponent* WaterBodyComponent)
{
if (OwnedWaterBodies.Find(WaterBodyComponent) == INDEX_NONE)
{
OwnedWaterBodies.Add(WaterBodyComponent);
EWaterZoneRebuildFlags RebuildFlags = EWaterZoneRebuildFlags::None;
if (WaterBodyComponent->AffectsWaterInfo())
{
RebuildFlags |= EWaterZoneRebuildFlags::UpdateWaterInfoTexture;
}
if (WaterBodyComponent->AffectsWaterMesh())
{
RebuildFlags |= EWaterZoneRebuildFlags::UpdateWaterMesh;
}
const FBox WaterBodyBounds = WaterBodyComponent->Bounds.GetBox();
MarkForRebuild(RebuildFlags, FBox2D(FVector2D(WaterBodyBounds.Min), FVector2D(WaterBodyBounds.Max)), /* DebugRequestingObject = */ WaterBodyComponent->GetOwner());
}
}
void AWaterZone::RemoveWaterBodyComponent(UWaterBodyComponent* WaterBodyComponent)
{
int32 Index;
if (OwnedWaterBodies.Find(WaterBodyComponent, Index))
{
OwnedWaterBodies.RemoveAtSwap(Index);
EWaterZoneRebuildFlags RebuildFlags = EWaterZoneRebuildFlags::None;
if (WaterBodyComponent->AffectsWaterInfo())
{
RebuildFlags |= EWaterZoneRebuildFlags::UpdateWaterInfoTexture;
}
if (WaterBodyComponent->AffectsWaterMesh())
{
RebuildFlags |= EWaterZoneRebuildFlags::UpdateWaterMesh;
}
const FBox WaterBodyBounds = WaterBodyComponent->Bounds.GetBox();
MarkForRebuild(RebuildFlags, FBox2D(FVector2D(WaterBodyBounds.Min), FVector2D(WaterBodyBounds.Max)), /* DebugRequestingObject = */ WaterBodyComponent->GetOwner());
}
}
FVector2D AWaterZone::GetZoneExtent() const
{
return ZoneExtent * FVector2D(GetActorScale());
}
void AWaterZone::Update()
{
TRACE_CPUPROFILER_EVENT_SCOPE(AWaterZone::Update);
if (bNeedsWaterInfoRebuild || (ForceUpdateWaterInfoNextFrames != 0))
{
ForceUpdateWaterInfoNextFrames = (ForceUpdateWaterInfoNextFrames < 0) ? ForceUpdateWaterInfoNextFrames : FMath::Max(0, ForceUpdateWaterInfoNextFrames - 1);
if (UpdateWaterInfoTexture())
{
bNeedsWaterInfoRebuild = false;
}
}
if (WaterMesh)
{
WaterMesh->Update();
}
}
#if WITH_EDITOR
void AWaterZone::ForceUpdateWaterInfoTexture()
{
UpdateWaterInfoTexture();
}
void AWaterZone::PostEditMove(bool bFinished)
{
Super::PostEditMove(bFinished);
// Ensure that the water mesh is rebuilt if it moves
EWaterZoneRebuildFlags RebuildFlags = EWaterZoneRebuildFlags::UpdateWaterInfoTexture | EWaterZoneRebuildFlags::UpdateWaterMesh;
UpdateOverlappingWaterBodies();
MarkForRebuild(RebuildFlags, /* DebugRequestingObject = */ this);
}
void AWaterZone::PostEditUndo()
{
Super::PostEditUndo();
UpdateOverlappingWaterBodies();
MarkForRebuild(EWaterZoneRebuildFlags::All, /* DebugRequestingObject = */ this);
}
void AWaterZone::PostEditImport()
{
Super::PostEditImport();
UpdateOverlappingWaterBodies();
MarkForRebuild(EWaterZoneRebuildFlags::All, /* DebugRequestingObject = */ this);
}
void AWaterZone::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
const FName PropertyName = PropertyChangedEvent.MemberProperty ? PropertyChangedEvent.MemberProperty->GetFName() : NAME_None;
if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, ZoneExtent))
{
OnExtentChanged();
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, BoundsComponent))
{
OnBoundsComponentModified();
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, RenderTargetResolution))
{
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, /* DebugRequestingObject = */ this);
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, bHalfPrecisionTexture))
{
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, /* DebugRequestingObject = */ this);
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, VelocityBlurRadius))
{
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, /* DebugRequestingObject = */ this);
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, LocalTessellationExtent))
{
MarkForRebuild(EWaterZoneRebuildFlags::All, /* DebugRequestingObject = */ this);
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, bEnableLocalOnlyTessellation))
{
MarkForRebuild(EWaterZoneRebuildFlags::All, /* DebugRequestingObject = */ this);
}
else if (PropertyName == USceneComponent::GetRelativeScale3DPropertyName())
{
MarkForRebuild(EWaterZoneRebuildFlags::All, /* DebugRequestingObject = */ this);
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AWaterZone, bAutoIncludeLandscapesAsTerrain))
{
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, /* DebugRequestingObject = */ this);
}
}
void AWaterZone::OnActorSelectionChanged(const TArray<UObject*>& NewSelection, bool bForceRefresh)
{
TArray<AWaterBody*> NewWaterBodiesSelection;
Algo::TransformIf(NewSelection, NewWaterBodiesSelection, [](UObject* Obj) { return Obj->IsA<AWaterBody>(); }, [](UObject* Obj) { return static_cast<AWaterBody*>(Obj); });
NewWaterBodiesSelection.Sort();
TArray<TWeakObjectPtr<AWaterBody>> NewWeakWaterBodiesSelection;
NewWeakWaterBodiesSelection.Reserve(NewWaterBodiesSelection.Num());
Algo::Transform(NewWaterBodiesSelection, NewWeakWaterBodiesSelection, [](AWaterBody* Body) { return TWeakObjectPtr<AWaterBody>(Body); });
// Ensure that the water mesh is rebuilt if water body selection changed
if (SelectedWaterBodies != NewWeakWaterBodiesSelection)
{
SelectedWaterBodies = NewWeakWaterBodiesSelection;
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterMesh | EWaterZoneRebuildFlags::UpdateWaterInfoTexture);
}
}
TUniquePtr<class FWorldPartitionActorDesc> AWaterZone::CreateClassActorDesc() const
{
return TUniquePtr<FWorldPartitionActorDesc>(new FWaterZoneActorDesc());
}
void AWaterZone::GetStreamingBounds(FBox& OutRuntimeBounds, FBox& OutEditorBounds) const
{
OutRuntimeBounds = OutEditorBounds = GetZoneBounds();
}
void AWaterZone::OnLandscapeHeightmapStreamed(const FOnHeightmapStreamedContext& InContext)
{
if (bAutoIncludeLandscapesAsTerrain)
{
UE_LOG(LogWater, Verbose, TEXT("AWaterZone::OnLandscapeHeightmapStreamed() -- Rebuilding Water Info Texture..."));
// Invalidate the region corresponding the heightmap being streamed in so that the water info is always up-to-date with the current landscape (i.e. ground) height :
const FBox2D& UpdateRegion = InContext.GetUpdateRegion();
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, FBox2D(FVector2D(UpdateRegion.Min), FVector2D(UpdateRegion.Max)), reinterpret_cast<const UObject*>(InContext.GetLandscape()));
}
}
void AWaterZone::OnLandscapeComponentDataChanged(ALandscapeProxy* InLandscape, const FLandscapeProxyComponentDataChangedParams& InChangeParams)
{
// TODO [jonathan.bard] : if that is a problem, we could add more info to FLandscapeProxyComponentDataChangedParams and tell exactly what has changed (heightmap, weightmap, etc.)
// because so far, the water info only needs to be notified about heightmap changes
if (bAutoIncludeLandscapesAsTerrain)
{
UE_LOG(LogWater, Verbose, TEXT("AWaterZone::OnLandscapeComponentDataChanged() -- Rebuilding Water Info Texture..."));
// Invalidate the region corresponding to each of the changed components. Use a combined region, because 99% of times, the component are contiguous and laid out in a square :
FBox CombinedBounds;
InChangeParams.ForEachComponent([&CombinedBounds](const ULandscapeComponent* InLandscapeComponent)
{
FBox LandscapeComponentBounds = InLandscapeComponent->GetLocalBounds().TransformBy(InLandscapeComponent->GetComponentTransform()).GetBox();
CombinedBounds += LandscapeComponentBounds;
});
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, FBox2D(FVector2D(CombinedBounds.Min), FVector2D(CombinedBounds.Max)), InLandscape);
}
}
#endif // WITH_EDITOR
void AWaterZone::SetFarMeshMaterial(UMaterialInterface* InFarDistanceMaterial)
{
if (WaterMesh && InFarDistanceMaterial != WaterMesh->FarDistanceMaterial)
{
WaterMesh->FarDistanceMaterial = InFarDistanceMaterial;
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterMesh, /* DebugRequestingObject = */ this);
}
}
void AWaterZone::OnExtentChanged()
{
#if WITH_EDITOR
BoundsComponent->SetBoxExtent(FVector(GetZoneExtent() / 2., 8192.f));
#endif // WITH_EDITOR
UpdateOverlappingWaterBodies();
MarkForRebuild(EWaterZoneRebuildFlags::All, /* DebugRequestingObject = */ this);
}
bool AWaterZone::UpdateOverlappingWaterBodies()
{
TArray<TWeakObjectPtr<UWaterBodyComponent>> OldOwnedWaterBodies = OwnedWaterBodies;
FWaterBodyManager::ForEachWaterBodyComponent(GetWorld(), [](UWaterBodyComponent* WaterBodyComponent)
{
WaterBodyComponent->UpdateWaterZones();
return true;
});
return (OwnedWaterBodies != OldOwnedWaterBodies);
}
#if WITH_EDITOR
void AWaterZone::OnBoundsComponentModified()
{
// BoxExtent is the radius of the box (half-extent).
const FVector2D NewBounds = 2.0 * FVector2D(BoundsComponent->GetUnscaledBoxExtent());
SetZoneExtent(NewBounds);
}
#endif // WITH_EDITOR
bool AWaterZone::UpdateWaterInfoTexture()
{
if (UWorld* World = GetWorld(); World && FApp::CanEverRender())
{
TRACE_CPUPROFILER_EVENT_SCOPE(AWaterZone::UpdateWaterInfoTexture);
float WaterZMin(TNumericLimits<float>::Max());
float WaterZMax(TNumericLimits<float>::Lowest());
// Collect a list of all materials used in the water info render to ensure they have complete shaders maps.
// If they do not, we must submit compile jobs for them and wait until they are finished before re-rendering.
TArray<UMaterialInterface*> UsedMaterials;
TArray<TWeakObjectPtr<UWaterBodyComponent>> WaterBodiesToRender;
ForEachWaterBodyComponent([World, &WaterBodiesToRender, &WaterZMax, &WaterZMin, &UsedMaterials](UWaterBodyComponent* WaterBodyComponent)
{
// skip components which don't affect the water info texture
if (!WaterBodyComponent->AffectsWaterInfo())
{
return true;
}
WaterBodiesToRender.Add(WaterBodyComponent);
const FBox WaterBodyBounds = WaterBodyComponent->Bounds.GetBox();
WaterZMax = FMath::Max(WaterZMax, WaterBodyBounds.Max.Z);
WaterZMin = FMath::Min(WaterZMin, WaterBodyBounds.Min.Z);
#if WITH_EDITOR
if (UMaterialInterface* WaterInfoMaterial = WaterBodyComponent->GetWaterInfoMaterialInstance())
{
UsedMaterials.Add(WaterInfoMaterial);
}
#endif // WITH_EDITOR
return true;
});
// If we don't have any water bodies we don't need to do anything.
if (WaterBodiesToRender.Num() == 0)
{
return true;
}
// Ensure that all the PSOs for the water info materials have been pre-cached before attempting to render the water info
// This is necessary if we have enabled the option to delay proxy creation until precache PSOs are ready, in which case
// we'd try to render the mesh components and end up skipping the creation of the proxy (or creating a temporary proxy
// with the default material fallback).
if (IsComponentPSOPrecachingEnabled() && ProxyCreationWhenPSOReady())
{
bool bHaveAllPSOsBeenCached = true;
for (const TWeakObjectPtr<UWaterBodyComponent>& WaterBodyComponentPtr : WaterBodiesToRender)
{
if (UWaterBodyComponent* WaterBodyComponent = WaterBodyComponentPtr.Get())
{
// CheckPSOPrecachingAndBoostPriority returns true if PSOs are still precaching.
if (UWaterBodyInfoMeshComponent* WaterInfoMeshComponent = WaterBodyComponent->GetWaterInfoMeshComponent())
{
bHaveAllPSOsBeenCached &= !WaterInfoMeshComponent->CheckPSOPrecachingAndBoostPriority();
}
if (UWaterBodyInfoMeshComponent* WaterInfoMeshComponent = WaterBodyComponent->GetDilatedWaterInfoMeshComponent())
{
bHaveAllPSOsBeenCached &= !WaterInfoMeshComponent->CheckPSOPrecachingAndBoostPriority();
}
}
}
// If the PSOs weren't fully pre-cached, we will try to render again on the next frame.
if (!bHaveAllPSOsBeenCached)
{
return false;
}
}
WaterHeightExtents = FVector2f(WaterZMin, WaterZMax);
// Only compute the ground min since we can use the water max z as the ground max z for more precision.
GroundZMin = TNumericLimits<float>::Max();
float GroundZMax = TNumericLimits<float>::Lowest();
TArray<TWeakObjectPtr<UPrimitiveComponent>> GroundPrimitiveComponents;
const FBox WaterZoneBounds = GetZoneBounds();
if (bAutoIncludeLandscapesAsTerrain)
{
for (ALandscapeProxy* LandscapeProxy : TActorRange<ALandscapeProxy>(World))
{
FBox LandscapeBox;
// Only collect the bounds for the ULandscapeComponents to avoid growing the bounding box unnecessarily
// if the landscape proxy has other components attached to it such as Grass or any other user-added components.
LandscapeProxy->ForEachComponent<ULandscapeComponent>(true, [&](const ULandscapeComponent* InLandscapeComp)
{
// Don't include bounds for non-registered components as they will not have the correct worldspace transform
if (InLandscapeComp->IsRegistered())
{
LandscapeBox += InLandscapeComp->Bounds.GetBox();
}
});
// Only consider landscapes which this zone intersects with in XY and if the landscape volume is not zero sized
if (WaterZoneBounds.IntersectXY(LandscapeBox) && LandscapeBox.GetVolume() > 0.0)
{
GroundZMin = FMath::Min(GroundZMin, LandscapeBox.Min.Z);
GroundZMax = FMath::Max(GroundZMax, LandscapeBox.Max.Z);
TInlineComponentArray<ULandscapeComponent*> LandscapeComponents(LandscapeProxy);
GroundPrimitiveComponents.Append(LandscapeComponents);
}
}
}
UWaterSubsystem* WaterSubsystem = UWaterSubsystem::GetWaterSubsystem(World);
check(WaterSubsystem);
TArray<UWaterTerrainComponent*> WaterTerrainComponents;
WaterSubsystem->GetWaterTerrainComponents(WaterTerrainComponents);
for (UWaterTerrainComponent* TerrainComponent : WaterTerrainComponents)
{
if (TerrainComponent->AffectsWaterZone(this))
{
TArray<UPrimitiveComponent*> TerrainPrimitives = TerrainComponent->GetTerrainPrimitives();
const FBox GroundActorBounds = TerrainComponent->GetOwner()->GetComponentsBoundingBox(true);
GroundZMin = FMath::Min(GroundZMin, GroundActorBounds.Min.Z);
GroundZMax = FMath::Max(GroundZMax, GroundActorBounds.Max.Z);
GroundPrimitiveComponents.Append(TerrainPrimitives);
}
}
// If we have no ground components we need to set GroundZMin to be something sensible because it won't have been set above.
if (GroundPrimitiveComponents.Num() == 0)
{
GroundZMax = WaterZMax;
GroundZMin = WaterZMin - CVarWaterFallbackDepth.GetValueOnGameThread();
}
#if WITH_EDITOR
// Check all the ground components have complete shader maps and are all compiled before we try to render them into the water info texture
for (TWeakObjectPtr<UPrimitiveComponent> GroundPrimCompPtr : GroundPrimitiveComponents)
{
if (UPrimitiveComponent* GroundPrimComp = GroundPrimCompPtr.Get())
{
if (GroundPrimComp->IsCompiling())
{
return false;
}
TArray<UMaterialInterface*> TmpUsedMaterials;
GroundPrimComp->GetUsedMaterials(TmpUsedMaterials, false);
UsedMaterials.Append(TmpUsedMaterials);
}
}
// Loop through all used materials and ensure that compile jobs are submitted for all which do not have complete shader maps before early-ing out of the info update.
bool bHasIncompleteShaderMaps = false;
const ERHIFeatureLevel::Type FeatureLevel = World->Scene->GetFeatureLevel();
for (UMaterialInterface* Material : UsedMaterials)
{
if (Material)
{
if (FMaterialResource* MaterialResource = Material->GetMaterialResource(FeatureLevel))
{
if (!MaterialResource->IsGameThreadShaderMapComplete())
{
MaterialResource->SubmitCompileJobs_GameThread(EShaderCompileJobPriority::High);
bHasIncompleteShaderMaps = true;
}
}
}
}
if (bHasIncompleteShaderMaps)
{
return false;
}
#endif // WITH_EDITOR
// The render path for rendering the water info texture without scene captures is executed within the scene renderer.
// If world rendering is disabled like in a loading screen, the scene renderer is not called and the water info texture will not be drawn.
UGameViewportClient* GameViewport = World->GetGameViewport();
// HACK [jonathan.bard] : CVarSkipWaterInfoTextureRenderWhenWorldRenderingDisabled is a temporary hack for MRQ because bDisableWorldRendering is true when capturing for MRQ.
// The proper solution is to refactor the water info texture and make it per-view (also for split screen support) : FWaterViewExtension::SetupViewFamily/SetupView
// are called on the game thread so they might be a good place for doing this.
// For now, we just use this CVar trick to let the water info texture be rendered when MRQ runs :
if ((CVarSkipWaterInfoTextureRenderWhenWorldRenderingDisabled.GetValueOnGameThread() != 0) && GameViewport && GameViewport->bDisableWorldRendering)
{
return false;
}
const ETextureRenderTargetFormat Format = bHalfPrecisionTexture ? ETextureRenderTargetFormat::RTF_RGBA16f : RTF_RGBA32f;
UTextureRenderTarget2DArray* OldTexture = WaterInfoTextureArray;
WaterInfoTextureArray = FWaterUtils::GetOrCreateTransientRenderTarget2DArray(OldTexture, TEXT("WaterInfoTexture"), RenderTargetResolution, WaterInfoTextureArrayNumSlices, Format);
// The water info texture is different, we need to bind the newly created texture to all registered water bodies
if (WaterInfoTextureArray != OldTexture)
{
OnWaterInfoTextureArrayCreated.Broadcast(WaterInfoTextureArray);
ForEachWaterBodyComponent([](UWaterBodyComponent* WaterBodyComponent)
{
WaterBodyComponent->UpdateMaterialInstances();
return true;
});
}
UE::WaterInfo::FRenderingContext Context;
Context.ZoneToRender = this;
Context.WaterBodies = WaterBodiesToRender;
Context.GroundPrimitiveComponents = MoveTemp(GroundPrimitiveComponents);
Context.CaptureZ = FMath::Max(WaterZMax, GroundZMax) + CaptureZOffset;
Context.TextureRenderTarget = WaterInfoTextureArray;
if (FWaterViewExtension* WaterViewExtension = UWaterSubsystem::GetWaterViewExtension(World))
{
WaterViewExtension->MarkWaterInfoTextureForRebuild(Context);
}
UE_LOG(LogWater, Verbose, TEXT("Water Zone (%s) queued Water Info texture update"), *GetNameSafe(this));
}
return true;
}
FVector AWaterZone::GetDynamicWaterInfoExtent() const
{
if (IsLocalOnlyTessellationEnabled())
{
return LocalTessellationExtent * GetActorScale();
}
else
{
// #todo_water [roey]: better implementation for 3D extent
return FVector(GetZoneExtent(), 0.0);
}
}
bool AWaterZone::GetDynamicWaterInfoCenter(int32 PlayerIndex, FVector& OutCenter) const
{
const UWorld* World = GetWorld();
check(World != nullptr);
bool bHasValidZoneLocation = false;
FVector Center = GetActorLocation();
if (const FWaterViewExtension* WaterViewExtension = UWaterSubsystem::GetWaterViewExtension(World))
{
bHasValidZoneLocation = WaterViewExtension->GetZoneLocation(this, PlayerIndex, Center);
}
return bHasValidZoneLocation;
}
bool AWaterZone::GetDynamicWaterInfoBounds(int32 PlayerIndex, FBox& OutBounds) const
{
const UWorld* World = GetWorld();
check(World != nullptr);
bool bHasValidZoneLocation = false;
FVector Center = GetActorLocation();
if (const FWaterViewExtension* WaterViewExtension = UWaterSubsystem::GetWaterViewExtension(World))
{
bHasValidZoneLocation = WaterViewExtension->GetZoneLocation(this, PlayerIndex, Center);
}
const FVector WaterInfoHalfExtents = GetDynamicWaterInfoExtent() / 2.;
OutBounds = FBox(Center - WaterInfoHalfExtents, Center + WaterInfoHalfExtents);
return bHasValidZoneLocation;
}
FVector AWaterZone::GetDynamicWaterInfoCenter() const
{
// The following index assume that there is no split screen and will request the position of the first player's water view.
// This call is a temporary fallback and is deprecated in favor of the overload which takes a specific player view.
constexpr int32 PlayerIndex = 0;
FVector Center;
GetDynamicWaterInfoCenter(PlayerIndex, Center);
return Center;
}
void AWaterZone::OnLevelAddedToWorld(ULevel* InLevel, UWorld* InWorld)
{
OnLevelChanged(InLevel, InWorld);
}
void AWaterZone::OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld)
{
OnLevelChanged(InLevel, InWorld);
}
bool AWaterZone::ContainsActorsAffectingWaterZone(ULevel* InLevel, const FBox& WaterZoneBounds) const
{
const bool bContainsActorsAffectingWaterZone = Algo::AnyOf(InLevel->Actors, [this, &WaterZoneBounds](const AActor* Actor)
{
return IsAffectingWaterZone(WaterZoneBounds, Actor);
});
return bContainsActorsAffectingWaterZone;
}
void AWaterZone::OnLevelChanged(ULevel* InLevel, UWorld* InWorld)
{
if ((InLevel == nullptr) || (InWorld != GetWorld()))
{
return;
}
TArray<FBox> WaterZoneViewsBounds;
GetAllDynamicWaterInfoBounds(WaterZoneViewsBounds);
bool bContainsActorsAffectingWaterZone = false;
if (WaterZoneViewsBounds.Num() > 0)
{
for (const FBox& ZoneViewBounds : WaterZoneViewsBounds)
{
if (ContainsActorsAffectingWaterZone(InLevel, ZoneViewBounds))
{
bContainsActorsAffectingWaterZone = true;
break;
}
}
}
else
{
bContainsActorsAffectingWaterZone = ContainsActorsAffectingWaterZone(InLevel, GetZoneBounds());
}
if (bContainsActorsAffectingWaterZone)
{
MarkForRebuild(EWaterZoneRebuildFlags::UpdateWaterInfoTexture, /* DebugRequestingObject = */ InLevel);
}
}
bool AWaterZone::IsAffectingWaterZone(const FBox& InWaterZoneBounds, const AActor* InActor) const
{
if (InActor == nullptr)
{
return false;
}
if (const AWaterBody* WaterBodyActor = Cast<const AWaterBody>(InActor))
{
if (const UWaterBodyComponent* WaterBodyComponent = WaterBodyActor->GetWaterBodyComponent())
{
if (WaterBodyComponent->GetWaterZone() == this && WaterBodyComponent->AffectsWaterInfo())
{
return InWaterZoneBounds.IntersectXY(WaterBodyComponent->Bounds.GetBox());
}
}
}
else if (const ALandscapeProxy* LandscapeProxy = Cast<const ALandscapeProxy>(InActor))
{
bool bIntersectsWaterZone = false;
LandscapeProxy->ForEachComponent<ULandscapeComponent>(false, [&bIntersectsWaterZone, &InWaterZoneBounds](const ULandscapeComponent* LandscapeComponent)
{
if (!bIntersectsWaterZone && (LandscapeComponent != nullptr))
{
const FBox LandscapeBox = LandscapeComponent->Bounds.GetBox();
bIntersectsWaterZone = InWaterZoneBounds.IntersectXY(LandscapeBox) && (LandscapeBox.GetVolume() > 0.0);
}
});
return bIntersectsWaterZone;
}
return false;
}