Files
2025-05-18 13:04:45 +08:00

185 lines
8.0 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "WaterUtils.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/TextureRenderTarget2DArray.h"
#include "UObject/Package.h"
#include "RenderingThread.h"
extern TAutoConsoleVariable<int32> CVarWaterEnabled;
extern TAutoConsoleVariable<int32> CVarWaterMeshEnabled;
extern TAutoConsoleVariable<int32> CVarWaterMeshEnableRendering;
static TAutoConsoleVariable<float> CVarWaterMaxFlowVelocity(
TEXT("r.Water.MaxFlowVelocity"),
1024.0f,
TEXT("The maximum magnitude for the velocity of a river to encode in the WaterInfo texture"),
ECVF_Default);
UMaterialInstanceDynamic* FWaterUtils::GetOrCreateTransientMID(UMaterialInstanceDynamic* InMID, FName InMIDName, UMaterialInterface* InMaterialInterface, EObjectFlags InAdditionalObjectFlags)
{
if (!IsValid(InMaterialInterface))
{
return nullptr;
}
UMaterialInstanceDynamic* ResultMID = InMID;
// If there's no MID yet or if the MID's parent material interface (could be a Material or a MIC) doesn't match the requested material interface (could be a Material, MIC or MID), create a new one :
if (!IsValid(InMID) || (InMID->Parent != InMaterialInterface))
{
// If the requested material is already a UMaterialInstanceDynamic, we can use it as is :
ResultMID = Cast<UMaterialInstanceDynamic>(InMaterialInterface);
if (ResultMID != nullptr)
{
ensure(EnumHasAnyFlags(InMaterialInterface->GetFlags(), EObjectFlags::RF_Transient)); // The name of the function implies we're dealing with transient MIDs
}
else
{
// If it's not a UMaterialInstanceDynamic, it's a UMaterialInstanceConstant or a UMaterial, both of which can be used to create a MID :
ResultMID = UMaterialInstanceDynamic::Create(InMaterialInterface, nullptr, MakeUniqueObjectName(GetTransientPackage(), UMaterialInstanceDynamic::StaticClass(), InMIDName));
ResultMID->SetFlags(InAdditionalObjectFlags);
}
}
check(ResultMID != nullptr);
return ResultMID;
}
UTextureRenderTarget2D* FWaterUtils::GetOrCreateTransientRenderTarget2D(UTextureRenderTarget2D* InRenderTarget, FName InRenderTargetName, const FIntPoint& InSize, ETextureRenderTargetFormat InFormat,
const FLinearColor& InClearColor, bool bInAutoGenerateMipMaps)
{
EPixelFormat PixelFormat = GetPixelFormatFromRenderTargetFormat(InFormat);
if ((InSize.X <= 0)
|| (InSize.Y <= 0)
|| (PixelFormat == EPixelFormat::PF_Unknown))
{
return nullptr;
}
if (IsValid(InRenderTarget))
{
if ((InRenderTarget->SizeX == InSize.X)
&& (InRenderTarget->SizeY == InSize.Y)
&& (InRenderTarget->GetFormat() == PixelFormat) // Watch out : GetFormat() returns a EPixelFormat (non-class enum), so we can't compare with a ETextureRenderTargetFormat
&& (InRenderTarget->ClearColor == InClearColor)
&& (InRenderTarget->bAutoGenerateMips == bInAutoGenerateMipMaps))
{
return InRenderTarget;
}
}
UTextureRenderTarget2D* NewRenderTarget2D = NewObject<UTextureRenderTarget2D>(GetTransientPackage(), MakeUniqueObjectName(GetTransientPackage(), UTextureRenderTarget2D::StaticClass(), InRenderTargetName));
check(NewRenderTarget2D);
NewRenderTarget2D->RenderTargetFormat = InFormat;
NewRenderTarget2D->ClearColor = InClearColor;
NewRenderTarget2D->bAutoGenerateMips = bInAutoGenerateMipMaps;
NewRenderTarget2D->InitAutoFormat(InSize.X, InSize.Y);
NewRenderTarget2D->UpdateResourceImmediate(true);
// Flush RHI thread after creating texture render target to make sure that RHIUpdateTextureReference is executed before doing any rendering with it
// This makes sure that Value->TextureReference.TextureReferenceRHI->GetReferencedTexture() is valid so that FUniformExpressionSet::FillUniformBuffer properly uses the texture for rendering, instead of using a fallback texture
ENQUEUE_RENDER_COMMAND(FlushRHIThreadToUpdateTextureRenderTargetReference)(
[](FRHICommandListImmediate& RHICmdList)
{
RHICmdList.ImmediateFlush(EImmediateFlushType::FlushRHIThread);
});
return NewRenderTarget2D;
}
WATER_API UTextureRenderTarget2DArray* FWaterUtils::GetOrCreateTransientRenderTarget2DArray(UTextureRenderTarget2DArray* InRenderTarget, FName InRenderTargetName, const FIntPoint& InSize, int32 InSlices, ETextureRenderTargetFormat InFormat, const FLinearColor& InClearColor, bool bInAutoGenerateMipMaps)
{
EPixelFormat PixelFormat = GetPixelFormatFromRenderTargetFormat(InFormat);
if ((InSize.X <= 0)
|| (InSize.Y <= 0)
|| (InSlices <= 0)
|| (PixelFormat == EPixelFormat::PF_Unknown))
{
return nullptr;
}
if (IsValid(InRenderTarget))
{
if ((InRenderTarget->SizeX == InSize.X)
&& (InRenderTarget->SizeY == InSize.Y)
&& (InRenderTarget->Slices == InSlices)
&& (InRenderTarget->GetFormat() == PixelFormat) // Watch out : GetFormat() returns a EPixelFormat (non-class enum), so we can't compare with a ETextureRenderTargetFormat
&& (InRenderTarget->ClearColor == InClearColor)
&& (InRenderTarget->bTargetArraySlicesIndependently))
{
return InRenderTarget;
}
}
UTextureRenderTarget2DArray* NewRenderTarget2DArray = NewObject<UTextureRenderTarget2DArray>(GetTransientPackage(), MakeUniqueObjectName(GetTransientPackage(), UTextureRenderTarget2DArray::StaticClass(), InRenderTargetName));
check(NewRenderTarget2DArray);
NewRenderTarget2DArray->ClearColor = InClearColor;
NewRenderTarget2DArray->bTargetArraySlicesIndependently = true; // When r.water.waterinfo.rendermethod is 1, we directly render into each individual slice
NewRenderTarget2DArray->Init(InSize.X, InSize.Y, InSlices, PixelFormat);
NewRenderTarget2DArray->UpdateResourceImmediate(true);
// Flush RHI thread after creating texture render target to make sure that RHIUpdateTextureReference is executed before doing any rendering with it
// This makes sure that Value->TextureReference.TextureReferenceRHI->GetReferencedTexture() is valid so that FUniformExpressionSet::FillUniformBuffer properly uses the texture for rendering, instead of using a fallback texture
ENQUEUE_RENDER_COMMAND(FlushRHIThreadToUpdateTextureRenderTargetReference)(
[](FRHICommandListImmediate& RHICmdList)
{
RHICmdList.ImmediateFlush(EImmediateFlushType::FlushRHIThread);
});
return NewRenderTarget2DArray;
}
FGuid FWaterUtils::StringToGuid(const FString& InStr)
{
// Compute a 128-bit hash based on the string and use that as a GUID :
FTCHARToUTF8 Converted(*InStr);
FMD5 MD5Gen;
MD5Gen.Update((const uint8*)Converted.Get(), Converted.Length());
uint32 Digest[4];
MD5Gen.Final((uint8*)Digest);
// FGuid::NewGuid() creates a version 4 UUID (at least on Windows), which will have the top 4 bits of the
// second field set to 0100. We'll set the top bit to 1 in the GUID we create, to ensure that we can never
// have a collision with other implicitly-generated GUIDs.
Digest[1] |= 0x80000000;
return FGuid(Digest[0], Digest[1], Digest[2], Digest[3]);
}
bool FWaterUtils::IsWaterEnabled(bool bIsRenderThread)
{
return !!(bIsRenderThread ? CVarWaterEnabled.GetValueOnRenderThread() : CVarWaterEnabled.GetValueOnGameThread());
}
bool FWaterUtils::IsWaterMeshEnabled(bool bIsRenderThread)
{
return IsWaterEnabled(bIsRenderThread) && !!(bIsRenderThread ? CVarWaterMeshEnabled.GetValueOnRenderThread() : CVarWaterMeshEnabled.GetValueOnGameThread());
}
bool FWaterUtils::IsWaterMeshRenderingEnabled(bool bIsRenderThread)
{
return IsWaterMeshEnabled(bIsRenderThread) && !!(bIsRenderThread ? CVarWaterMeshEnableRendering.GetValueOnRenderThread() : CVarWaterMeshEnableRendering.GetValueOnGameThread());
}
float FWaterUtils::GetWaterMaxFlowVelocity(bool bIsRenderThread)
{
return (bIsRenderThread ? CVarWaterMaxFlowVelocity.GetValueOnRenderThread() : CVarWaterMaxFlowVelocity.GetValueOnGameThread());
}
FVector4f FWaterUtils::PackFlowData(float VelocityMagnitude, float DirectionAngle)
{
check((DirectionAngle >= 0.f) && (DirectionAngle <= TWO_PI));
checkf(VelocityMagnitude >= 0., TEXT("We expect a positive magnitude"));
const float MaxVelocity = GetWaterMaxFlowVelocity(false);
FVector4f Result;
Result.X = FMath::Clamp(VelocityMagnitude / MaxVelocity, 0.f, 1.f);
Result.Y = FMath::Clamp(DirectionAngle / TWO_PI, 0.f, 1.f);
return Result;
}