Files
UnrealEngine/Engine/Source/Runtime/Renderer/Private/IndirectLightingCache.cpp
2025-05-18 13:04:45 +08:00

1192 lines
50 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
Implements a volume texture atlas for caching indirect lighting on a per-object basis
=============================================================================*/
#include "CoreMinimal.h"
#include "Stats/Stats.h"
#include "HAL/IConsoleManager.h"
#include "Async/TaskGraphInterfaces.h"
#include "RHI.h"
#include "RenderResource.h"
#include "SceneTypes.h"
#include "RendererInterface.h"
#include "Math/GenericOctree.h"
#include "PrimitiveSceneInfo.h"
#include "DynamicPrimitiveDrawing.h"
#include "ScenePrivate.h"
#include "PrecomputedLightVolume.h"
#include "RenderCore.h"
#include "UnrealEngine.h"
/**
* Primitive bounds size will be rounded up to the next value of Pow(BoundSizeRoundUpBase, N) and N is an integer.
* This provides some stability as bounds get larger and smaller, although by adding some waste.
*/
const float BoundSizeRoundUpBase = FMath::Sqrt(2.f);
const float LogEBoundSizeRoundUpBase = FMath::Loge(BoundSizeRoundUpBase);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Console variables that can be changed at runtime to configure or debug the indirect lighting cache
//////////////////////////////////////////////////////////////////////////////////////////////////////////
int32 GCacheDrawLightingSamples = 0;
static FAutoConsoleVariableRef CVarCacheDrawLightingSamples(
TEXT("r.Cache.DrawLightingSamples"),
GCacheDrawLightingSamples,
TEXT("Whether to draw indirect lighting sample points as generated by Lightmass.\n")
TEXT("0 is off (default), 1 is on"),
ECVF_RenderThreadSafe
);
int32 GCacheDrawDirectionalShadowing = 0;
static FAutoConsoleVariableRef CVarCacheDrawDirectionalShadowing(
TEXT("r.Cache.DrawDirectionalShadowing"),
GCacheDrawDirectionalShadowing,
TEXT("Whether to draw direct shadowing sample points as generated by Lightmass.\n")
TEXT("0 is off (default), 1 is on"),
ECVF_RenderThreadSafe
);
int32 GCacheDrawInterpolationPoints = 0;
static FAutoConsoleVariableRef CVarCacheDrawInterpolationPoints(
TEXT("r.Cache.DrawInterpolationPoints"),
GCacheDrawInterpolationPoints,
TEXT("Whether to draw positions that indirect lighting is interpolated at when they are updated, which are stored in the cache.\n")
TEXT("Probably need 'r.CacheUpdateEveryFrame 1' as well to be useful, otherwise points will flicker as they update.\n")
TEXT("0 is off (default), 1 is on"),
ECVF_RenderThreadSafe
);
int32 GCacheUpdateEveryFrame = 0;
static FAutoConsoleVariableRef CVarCacheUpdateEveryFrame(
TEXT("r.Cache.UpdateEveryFrame"),
GCacheUpdateEveryFrame,
TEXT("Whether to update indirect lighting cache allocations every frame, even if they would have been cached. 0 is off (default), 1 is on"),
ECVF_RenderThreadSafe
);
float GSingleSampleTransitionSpeed = 800;
static FAutoConsoleVariableRef CVarSingleSampleTransitionSpeed(
TEXT("r.Cache.SampleTransitionSpeed"),
GSingleSampleTransitionSpeed,
TEXT("When using single sample lighting, controls the speed of the transition between two point samples (fade over time)."),
ECVF_RenderThreadSafe
);
int32 GCacheReduceSHRinging = 1;
static FAutoConsoleVariableRef CVarCacheReduceSHRinging(
TEXT("r.Cache.ReduceSHRinging"),
GCacheReduceSHRinging,
TEXT("Whether to modify indirect lighting cache SH samples to reduce ringing. 0 is off, 1 is on (default)"),
ECVF_RenderThreadSafe
);
int32 GIndirectLightingCache = 1;
static FAutoConsoleVariableRef CVarIndirectLightingCache(
TEXT("r.IndirectLightingCache"),
GIndirectLightingCache,
TEXT("Whether to use the indirect lighting cache on dynamic objects. 0 is off, 1 is on (default)"),
ECVF_RenderThreadSafe
);
int32 GCacheQueryNodeLevel = 3;
static FAutoConsoleVariableRef CVarCacheQueryNodeLevel(
TEXT("r.Cache.QueryNodeLevel"),
GCacheQueryNodeLevel,
TEXT("Level of the lighting sample octree whose node's extents should be the target size for queries into the octree.\n")
TEXT("Primitive blocks will be broken up into multiple octree queries if they are larger than this.")
TEXT("0 is the root, 12 is the leaf level"),
ECVF_RenderThreadSafe
);
int32 GCacheLimitQuerySize = 1;
static FAutoConsoleVariableRef CVarCacheLimitQuerySize(
TEXT("r.Cache.LimitQuerySize"),
GCacheLimitQuerySize,
TEXT("0 is off, 1 is on (default)"),
ECVF_RenderThreadSafe
);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Console variables that cannot be changed at runtime
// These are console variables so their values can be read from an ini
//////////////////////////////////////////////////////////////////////////////////////////////////////////
int32 GLightingCacheMovableObjectAllocationSize = 5;
static FAutoConsoleVariableRef CVarLightingCacheMovableObjectAllocationSize(
TEXT("r.Cache.LightingCacheMovableObjectAllocationSize"),
GLightingCacheMovableObjectAllocationSize,
TEXT("Resolution of the interpolation sample volume used to light a dynamic object. \n")
TEXT("Values of 1 or 2 will result in a single interpolation sample per object which does not provide continuous lighting under movement, so interpolation is done over time. \n")
TEXT("Values of 3 or more support the necessary padding to provide continuous results under movement."),
ECVF_ReadOnly
);
// PS4 wants multiple of 16 for fast RHIUpdateTexture3D
int32 GLightingCacheDimension = 64;
static FAutoConsoleVariableRef CVarLightingCacheDimension(
TEXT("r.Cache.LightingCacheDimension"),
GLightingCacheDimension,
TEXT("Dimensions of the lighting cache. This should be a multiple of r.LightingCacheMovableObjectAllocationSize for least waste."),
ECVF_ReadOnly
);
bool IsIndirectLightingCacheAllowed(ERHIFeatureLevel::Type InFeatureLevel)
{
return GIndirectLightingCache != 0 && IsStaticLightingAllowed();
}
bool CanIndirectLightingCacheUseVolumeTexture(ERHIFeatureLevel::Type InFeatureLevel)
{
// @todo Mac OS X/OpenGL: For OpenGL devices which don't support volume-texture rendering we need to use the simpler point indirect lighting shaders.
return InFeatureLevel >= ERHIFeatureLevel::SM5 && RHISupportsVolumeTextureRendering(GetFeatureLevelShaderPlatform(InFeatureLevel));
}
FIndirectLightingCache::FIndirectLightingCache(ERHIFeatureLevel::Type InFeatureLevel)
: FRenderResource(InFeatureLevel)
, bUpdateAllCacheEntries(true)
, BlockAllocator(0, 0, 0, GLightingCacheDimension, GLightingCacheDimension, GLightingCacheDimension, false, false)
{
NextPointId = GLightingCacheDimension + 1;
CacheSize = GLightingCacheDimension;
}
void FIndirectLightingCache::InitRHI(FRHICommandListBase&)
{
if (CanIndirectLightingCacheUseVolumeTexture(GetFeatureLevel()))
{
FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
ETextureCreateFlags Flags = TexCreate_ShaderResource | TexCreate_NoTiling;
FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::CreateVolumeDesc(
CacheSize,
CacheSize,
CacheSize,
PF_FloatRGBA,
FClearValueBinding::None,
Flags,
TexCreate_None,
false,
1));
GRenderTargetPool.FindFreeElement(RHICmdList, Desc, Texture0, TEXT("IndirectLightingCache_0"));
GRenderTargetPool.FindFreeElement(RHICmdList, Desc, Texture1, TEXT("IndirectLightingCache_1"));
GRenderTargetPool.FindFreeElement(RHICmdList, Desc, Texture2, TEXT("IndirectLightingCache_2"));
}
}
void FIndirectLightingCache::ReleaseRHI()
{
GRenderTargetPool.FreeUnusedResource(Texture0);
GRenderTargetPool.FreeUnusedResource(Texture1);
GRenderTargetPool.FreeUnusedResource(Texture2);
}
static bool IsTexelMinValid(FIntVector TexelMin)
{
return TexelMin.X >= 0 && TexelMin.Y >= 0 && TexelMin.Z >= 0;
}
FIndirectLightingCacheBlock& FIndirectLightingCache::FindBlock(FIntVector TexelMin)
{
checkSlow(IsTexelMinValid(TexelMin));
return VolumeBlocks.FindChecked(TexelMin);
}
const FIndirectLightingCacheBlock& FIndirectLightingCache::FindBlock(FIntVector TexelMin) const
{
checkSlow(IsTexelMinValid(TexelMin));
return VolumeBlocks.FindChecked(TexelMin);
}
void FIndirectLightingCache::DeallocateBlock(FIntVector Min, int32 Size)
{
if (Size > 1)
{
verify(BlockAllocator.RemoveElement(Min.X, Min.Y, Min.Z, Size, Size, Size));
}
VolumeBlocks.Remove(Min);
}
bool FIndirectLightingCache::AllocateBlock(int32 Size, FIntVector& OutMin)
{
if (Size == 1)
{
// Assign a min that won't overlap with any of the samples allocated from the volume texture, so we can be added to VolumeBlocks without collisions
// This min is not used for anything else for point samples
OutMin = FIntVector(NextPointId, 0, 0);
NextPointId++;
// Point samples don't go through the volume texture, allocation always succeeds
return true;
}
else
{
return BlockAllocator.AddElement((uint32&)OutMin.X, (uint32&)OutMin.Y, (uint32&)OutMin.Z, Size, Size, Size);
}
}
void FIndirectLightingCache::CalculateBlockPositionAndSize(const FBoxSphereBounds& Bounds, int32 TexelSize, FVector& OutMin, FVector& OutSize) const
{
FVector RoundedBoundsSize;
// Find the exponent needed to represent the bounds size if BoundSizeRoundUpBase is the base
RoundedBoundsSize.X = FMath::Max(1.f, FMath::Loge(Bounds.BoxExtent.X * 2) / LogEBoundSizeRoundUpBase);
RoundedBoundsSize.Y = FMath::Max(1.f, FMath::Loge(Bounds.BoxExtent.Y * 2) / LogEBoundSizeRoundUpBase);
RoundedBoundsSize.Z = FMath::Max(1.f, FMath::Loge(Bounds.BoxExtent.Z * 2) / LogEBoundSizeRoundUpBase);
// Round up to the next integer exponent to provide stability even when Bounds.BoxExtent is changing
RoundedBoundsSize.X = FMath::Pow(BoundSizeRoundUpBase, FMath::TruncToInt(RoundedBoundsSize.X) + 1);
RoundedBoundsSize.Y = FMath::Pow(BoundSizeRoundUpBase, FMath::TruncToInt(RoundedBoundsSize.Y) + 1);
RoundedBoundsSize.Z = FMath::Pow(BoundSizeRoundUpBase, FMath::TruncToInt(RoundedBoundsSize.Z) + 1);
// For single sample allocations, use an effective texel size of 5 for snapping
const int32 EffectiveTexelSize = TexelSize > 2 ? TexelSize : 5;
// Setup a cell size that positions will be snapped to, in world space
// The block allocation has to be padded by one texel in world space, twice
// First to handle having snapped the allocation min to the next lowest cell size
// Second to provide padding to handle trilinear volume texture filtering
// Hence the 'EffectiveTexelSize - 2'
const FVector CellSize = RoundedBoundsSize / (EffectiveTexelSize - 2);
const FVector BoundsMin = Bounds.Origin - Bounds.BoxExtent;
if (TexelSize > 2)
{
FVector SnappedMin;
SnappedMin.X = CellSize.X * FMath::FloorToFloat(BoundsMin.X / CellSize.X);
SnappedMin.Y = CellSize.Y * FMath::FloorToFloat(BoundsMin.Y / CellSize.Y);
SnappedMin.Z = CellSize.Z * FMath::FloorToFloat(BoundsMin.Z / CellSize.Z);
// Shift the min down so that the center of the voxel is at the min
// This is necessary so that all pixels inside the bounds only interpolate from valid voxels
SnappedMin -= CellSize * .5f;
const FVector SnappedSize = TexelSize * CellSize;
OutMin = SnappedMin;
OutSize = SnappedSize;
}
else
{
FVector SnappedCenter;
SnappedCenter.X = CellSize.X * FMath::FloorToFloat((Bounds.Origin.X + .5f * CellSize.X) / CellSize.X);
SnappedCenter.Y = CellSize.Y * FMath::FloorToFloat((Bounds.Origin.Y + .5f * CellSize.Y) / CellSize.Y);
SnappedCenter.Z = CellSize.Z * FMath::FloorToFloat((Bounds.Origin.Z + .5f * CellSize.Z) / CellSize.Z);
// Place the min at the snapped center of the object, which will be most representative for single sample lighting
OutMin = SnappedCenter;
OutSize = FVector(0);
}
}
void FIndirectLightingCache::CalculateBlockScaleAndAdd(FIntVector InTexelMin, int32 AllocationTexelSize, FVector InMin, FVector InSize, FVector& OutScale, FVector& OutAdd, FVector& OutMinUV, FVector& OutMaxUV) const
{
const FVector MinUV(InTexelMin.X / (float)CacheSize, InTexelMin.Y / (float)CacheSize, InTexelMin.Z / (float)CacheSize);
// Half texel offset to make sure we don't read from texels in other allocations through filtering
OutMinUV = MinUV + FVector(.5f / CacheSize);
if (AllocationTexelSize > 2)
{
const float UVSize = AllocationTexelSize / (float)CacheSize;
// need to remove 0
if (InSize.X == 0.f)
{
InSize.X = 0.01f;
}
if(InSize.Y == 0.f)
{
InSize.Y = 0.01f;
}
if(InSize.Z == 0.f)
{
InSize.Z = 0.01f;
}
// Setup a scale and add to convert from world space position to volume texture UV
OutScale = FVector(UVSize) / InSize;
OutAdd = -InMin * UVSize / InSize + MinUV;
// Half texel offset to make sure we don't read from texels in other allocations through filtering
OutMaxUV = MinUV + UVSize - FVector(.5f / CacheSize);
}
else
{
// All pixels sample from center of texel so that neighbors don't contribute, since there's no padding
OutScale = FVector(0);
OutAdd = MinUV + FVector(.5f / CacheSize);
OutMaxUV = OutMinUV;
}
}
FIndirectLightingCacheAllocation* FIndirectLightingCache::AllocatePrimitive(const FPrimitiveSceneInfo* PrimitiveSceneInfo, bool bUnbuiltPreview)
{
const bool bPointSample = PrimitiveSceneInfo->Proxy->GetIndirectLightingCacheQuality() == ILCQ_Point || bUnbuiltPreview;
const int32 BlockSize = bPointSample ? 1 : GLightingCacheMovableObjectAllocationSize;
return PrimitiveAllocations.Add(PrimitiveSceneInfo->PrimitiveComponentId, CreateAllocation(BlockSize, PrimitiveSceneInfo->Proxy->GetBounds(), bPointSample, bUnbuiltPreview));
}
FIndirectLightingCacheAllocation* FIndirectLightingCache::CreateAllocation(int32 BlockSize, const FBoxSphereBounds& Bounds, bool bPointSample, bool bUnbuiltPreview)
{
check(BlockSize > 1 || bPointSample);
FIndirectLightingCacheAllocation* NewAllocation = new FIndirectLightingCacheAllocation();
FIndirectLightingCacheBlock NewBlock;
//@todo - don't allocate point samples from the layout, they don't go through the volume texture
if (AllocateBlock(BlockSize, NewBlock.MinTexel))
{
NewBlock.TexelSize = BlockSize;
CalculateBlockPositionAndSize(Bounds, BlockSize, NewBlock.Min, NewBlock.Size);
FVector Scale;
FVector Add;
FVector MinUV;
FVector MaxUV;
CalculateBlockScaleAndAdd(NewBlock.MinTexel, NewBlock.TexelSize, NewBlock.Min, NewBlock.Size, Scale, Add, MinUV, MaxUV);
VolumeBlocks.Add(NewBlock.MinTexel, NewBlock);
NewAllocation->SetParameters(NewBlock.MinTexel, NewBlock.TexelSize, Scale, Add, MinUV, MaxUV, bPointSample, bUnbuiltPreview);
checkf(NewAllocation->AllocationTexelSize > 1 || NewAllocation->bPointSample, TEXT("%i, %i"), NewAllocation->AllocationTexelSize, NewAllocation->bPointSample ? 1 : 0);
}
return NewAllocation;
}
void FIndirectLightingCache::ReleasePrimitive(FPrimitiveComponentId PrimitiveId)
{
FIndirectLightingCacheAllocation* PrimitiveAllocation;
if (PrimitiveAllocations.RemoveAndCopyValue(PrimitiveId, PrimitiveAllocation))
{
check(PrimitiveAllocation);
if (PrimitiveAllocation->IsValid())
{
DeallocateBlock(PrimitiveAllocation->MinTexel, PrimitiveAllocation->AllocationTexelSize);
}
delete PrimitiveAllocation;
}
}
FIndirectLightingCacheAllocation* FIndirectLightingCache::FindPrimitiveAllocation(FPrimitiveComponentId PrimitiveId) const
{
return PrimitiveAllocations.FindRef(PrimitiveId);
}
FAutoConsoleTaskPriority CPrio_FUpdateCachePrimitivesTask(
TEXT("TaskGraph.TaskPriorities.UpdateCachePrimitivesTask"),
TEXT("Task and thread priority for FUpdateCachePrimitivesTask."),
ENamedThreads::HighThreadPriority, // if we have high priority task threads, then use them...
ENamedThreads::NormalTaskPriority, // .. at normal task priority
ENamedThreads::HighTaskPriority // if we don't have hi pri threads, then use normal priority threads at high task priority instead
);
class FUpdateCachePrimitivesTask
{
FIndirectLightingCache* ILC;
FScene* Scene;
FSceneRenderer& Renderer;
TMap<FIntVector, FBlockUpdateInfo>& BlocksToUpdate;
TArray<FIndirectLightingCacheAllocation*>& TransitionsOverTimeToUpdate;
TArray<FPrimitiveSceneInfo*>& PrimitivesToUpdateStaticMeshes;
bool bAllowUnbuiltPreview;
public:
FUpdateCachePrimitivesTask(
FIndirectLightingCache* InILC,
FScene* InScene, FSceneRenderer& InRenderer,
bool bInAllowUnbuiltPreview,
TMap<FIntVector, FBlockUpdateInfo>& OutBlocksToUpdate,
TArray<FIndirectLightingCacheAllocation*>& OutTransitionsOverTimeToUpdate,
TArray<FPrimitiveSceneInfo*>& OutPrimitivesToUpdateStaticMeshes)
: ILC(InILC)
, Scene(InScene)
, Renderer(InRenderer)
, BlocksToUpdate(OutBlocksToUpdate)
, TransitionsOverTimeToUpdate(OutTransitionsOverTimeToUpdate)
, PrimitivesToUpdateStaticMeshes(OutPrimitivesToUpdateStaticMeshes)
, bAllowUnbuiltPreview(bInAllowUnbuiltPreview)
{
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FUpdateCachePrimitivesTask, STATGROUP_TaskGraphTasks);
}
ENamedThreads::Type GetDesiredThread()
{
return CPrio_FUpdateCachePrimitivesTask.Get();
}
static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; }
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
ILC->UpdateCachePrimitivesInternal(Scene, Renderer, bAllowUnbuiltPreview, BlocksToUpdate, TransitionsOverTimeToUpdate, PrimitivesToUpdateStaticMeshes);
}
};
void FIndirectLightingCache::StartUpdateCachePrimitivesTask(FScene* Scene, FSceneRenderer& Renderer, bool bAllowUnbuiltPreview, FILCUpdatePrimTaskData& OutTaskData)
{
OutTaskData.TaskRef = TGraphTask<FUpdateCachePrimitivesTask>::CreateTask().ConstructAndDispatchWhenReady(
this,
Scene,
Renderer,
bAllowUnbuiltPreview,
OutTaskData.OutBlocksToUpdate,
OutTaskData.OutTransitionsOverTimeToUpdate,
OutTaskData.OutPrimitivesToUpdateStaticMeshes
);
}
void FIndirectLightingCache::FinalizeCacheUpdates(FScene* Scene, FSceneRenderer& Renderer, FILCUpdatePrimTaskData& TaskData)
{
SCOPE_CYCLE_COUNTER(STAT_UpdateIndirectLightingCacheFinalize);
FTaskGraphInterface::Get().WaitUntilTaskCompletes(TaskData.TaskRef, ENamedThreads::GetRenderThread_Local());
FinalizeUpdateInternal_RenderThread(Scene, Renderer, TaskData.OutBlocksToUpdate, TaskData.OutTransitionsOverTimeToUpdate, TaskData.OutPrimitivesToUpdateStaticMeshes);
}
void FIndirectLightingCache::UpdateCache(FScene* Scene, FSceneRenderer& Renderer, bool bAllowUnbuiltPreview)
{
SCOPE_CYCLE_COUNTER(STAT_UpdateIndirectLightingCache);
TMap<FIntVector, FBlockUpdateInfo>BlocksToUpdate;
TArray<FIndirectLightingCacheAllocation*> TransitionsOverTimeToUpdate;
TArray<FPrimitiveSceneInfo*> PrimitivesToUpdateStaticMeshes;
UpdateCachePrimitivesInternal(Scene, Renderer, bAllowUnbuiltPreview, BlocksToUpdate, TransitionsOverTimeToUpdate, PrimitivesToUpdateStaticMeshes);
FinalizeUpdateInternal_RenderThread(Scene, Renderer, BlocksToUpdate, TransitionsOverTimeToUpdate, PrimitivesToUpdateStaticMeshes);
}
bool FIndirectLightingCache::IndirectLightingAllowed(FScene* Scene, FSceneRenderer& Renderer) const
{
bool bAnyViewAllowsIndirectLightingCache = false;
if (IsIndirectLightingCacheAllowed(GetFeatureLevel()) && Scene->PrecomputedLightVolumes.Num() > 0)
{
for (int32 ViewIndex = 0; ViewIndex < Renderer.Views.Num(); ViewIndex++)
{
bAnyViewAllowsIndirectLightingCache |= (bool)Renderer.Views[ViewIndex].Family->EngineShowFlags.IndirectLightingCache;
}
}
return bAnyViewAllowsIndirectLightingCache;
}
void FIndirectLightingCache::ProcessPrimitiveUpdate(FScene* Scene, FViewInfo& View, int32 PrimitiveIndex, bool bAllowUnbuiltPreview, bool bAllowVolumeSample, TMap<FIntVector, FBlockUpdateInfo>& OutBlocksToUpdate, TArray<FIndirectLightingCacheAllocation*>& OutTransitionsOverTimeToUpdate, TArray<FPrimitiveSceneInfo*>& OutPrimitivesToUpdateStaticMeshes)
{
FPrimitiveSceneInfo* PrimitiveSceneInfo = Scene->Primitives[PrimitiveIndex];
const bool bIndirectLightingCacheBufferWasDirty = PrimitiveSceneInfo->NeedsIndirectLightingCacheBufferUpdate();
const TMap<FPrimitiveComponentId, FAttachmentGroupSceneInfo>& AttachmentGroups = Scene->AttachmentGroups;
UpdateCachePrimitive(AttachmentGroups, PrimitiveSceneInfo, bAllowUnbuiltPreview, bAllowVolumeSample, OutBlocksToUpdate, OutTransitionsOverTimeToUpdate, OutPrimitivesToUpdateStaticMeshes);
// If it was already dirty, then the primitive is already in one of the view dirty primitive list at this point.
// This also ensures that a primitive does not get added twice to the list, which could create an array reallocation.
if (!bIndirectLightingCacheBufferWasDirty && PrimitiveSceneInfo->NeedsIndirectLightingCacheBufferUpdate())
{
View.DirtyIndirectLightingCacheBufferPrimitives.Push(PrimitiveSceneInfo);
}
}
void FIndirectLightingCache::UpdateCachePrimitivesInternal(FScene* Scene, FSceneRenderer& Renderer, bool bAllowUnbuiltPreview, TMap<FIntVector, FBlockUpdateInfo>& OutBlocksToUpdate, TArray<FIndirectLightingCacheAllocation*>& OutTransitionsOverTimeToUpdate, TArray<FPrimitiveSceneInfo*>& OutPrimitivesToUpdateStaticMeshes)
{
SCOPE_CYCLE_COUNTER(STAT_UpdateIndirectLightingCachePrims);
const TMap<FPrimitiveComponentId, FAttachmentGroupSceneInfo>& AttachmentGroups = Scene->AttachmentGroups;
if (IndirectLightingAllowed(Scene, Renderer))
{
for (FViewInfo& View : Renderer.Views)
{
View.DirtyIndirectLightingCacheBufferPrimitivesMutex.Lock();
}
if (bUpdateAllCacheEntries)
{
const uint32 PrimitiveCount = Scene->Primitives.Num();
for (uint32 PrimitiveIndex = 0; PrimitiveIndex < PrimitiveCount; ++PrimitiveIndex)
{
FPrimitiveSceneInfo* PrimitiveSceneInfo = Scene->Primitives[PrimitiveIndex];
const bool bIndirectLightingCacheBufferWasDirty = PrimitiveSceneInfo->NeedsIndirectLightingCacheBufferUpdate();
UpdateCachePrimitive(AttachmentGroups, PrimitiveSceneInfo, false, true, OutBlocksToUpdate, OutTransitionsOverTimeToUpdate, OutPrimitivesToUpdateStaticMeshes);
// If it was already dirty, then the primitive is already in one of the view dirty primitive list at this point.
if (!bIndirectLightingCacheBufferWasDirty)
{
PrimitiveSceneInfo->MarkIndirectLightingCacheBufferDirty();
// Check if it is visible otherwise, it will be updated next time it is visible.
for (int32 ViewIndex = 0; ViewIndex < Renderer.Views.Num(); ViewIndex++)
{
FViewInfo& View = Renderer.Views[ViewIndex];
if (View.PrimitiveVisibilityMap[PrimitiveIndex])
{
View.DirtyIndirectLightingCacheBufferPrimitives.Push(PrimitiveSceneInfo);
break; // We only need to add it in one of the view list.
}
}
}
}
}
else
{
for (int32 ViewIndex = 0; ViewIndex < Renderer.Views.Num(); ViewIndex++)
{
FViewInfo& View = Renderer.Views[ViewIndex];
for (FSceneSetBitIterator BitIt(View.PrimitiveVisibilityMap); BitIt; ++BitIt)
{
uint32 PrimitiveIndex = BitIt.GetIndex();
// FDrawTranslucentMeshAction::AllowIndirectLightingCacheVolumeTexture doesn't allow volume samples on translucency, so we only need to support one if the primitive has at least one opaque material
const bool bAllowVolumeSample = View.PrimitiveViewRelevanceMap[PrimitiveIndex].bOpaque;
ProcessPrimitiveUpdate(Scene, View, PrimitiveIndex, bAllowUnbuiltPreview, bAllowVolumeSample, OutBlocksToUpdate, OutTransitionsOverTimeToUpdate, OutPrimitivesToUpdateStaticMeshes);
}
// Any visible primitives with an indirect shadow need their ILC updated, since that determines the indirect shadow direction
for (int32 IndirectPrimitiveIndex = 0; IndirectPrimitiveIndex < View.IndirectShadowPrimitives.Num(); IndirectPrimitiveIndex++)
{
int32 PrimitiveIndex = View.IndirectShadowPrimitives[IndirectPrimitiveIndex]->GetIndex();
if (!View.PrimitiveVisibilityMap[PrimitiveIndex])
{
ProcessPrimitiveUpdate(Scene, View, PrimitiveIndex, bAllowUnbuiltPreview, true, OutBlocksToUpdate, OutTransitionsOverTimeToUpdate, OutPrimitivesToUpdateStaticMeshes);
}
}
}
}
for (FViewInfo& View : Renderer.Views)
{
View.DirtyIndirectLightingCacheBufferPrimitivesMutex.Unlock();
}
bUpdateAllCacheEntries = false;
}
}
void FIndirectLightingCache::FinalizeUpdateInternal_RenderThread(FScene* Scene, FSceneRenderer& Renderer, TMap<FIntVector, FBlockUpdateInfo>& BlocksToUpdate, const TArray<FIndirectLightingCacheAllocation*>& TransitionsOverTimeToUpdate, TArray<FPrimitiveSceneInfo*>& PrimitivesToUpdateStaticMeshes)
{
check(IsInRenderingThread());
if (IndirectLightingAllowed(Scene, Renderer))
{
{
SCOPE_CYCLE_COUNTER(STAT_UpdateIndirectLightingCacheBlocks);
UpdateBlocks(Scene, Renderer.Views.GetData(), BlocksToUpdate);
}
{
SCOPE_CYCLE_COUNTER(STAT_UpdateIndirectLightingCacheTransitions);
UpdateTransitionsOverTime(TransitionsOverTimeToUpdate, Renderer.ViewFamily.Time.GetDeltaWorldTimeSeconds());
}
for (int32 PrimitiveIndex = 0; PrimitiveIndex < PrimitivesToUpdateStaticMeshes.Num(); ++PrimitiveIndex)
{
PrimitivesToUpdateStaticMeshes[PrimitiveIndex]->RequestStaticMeshUpdate();
}
}
if (GCacheDrawLightingSamples || Renderer.ViewFamily.EngineShowFlags.VolumeLightingSamples || GCacheDrawDirectionalShadowing)
{
FViewElementPDI DebugPDI(&Renderer.Views[0], nullptr, &Renderer.Views[0].DynamicPrimitiveCollector);
for (int32 VolumeIndex = 0; VolumeIndex < Scene->PrecomputedLightVolumes.Num(); VolumeIndex++)
{
const FPrecomputedLightVolume* PrecomputedLightVolume = Scene->PrecomputedLightVolumes[VolumeIndex];
PrecomputedLightVolume->DebugDrawSamples(&DebugPDI, GCacheDrawDirectionalShadowing != 0);
}
}
}
bool FIndirectLightingCache::UpdateCacheAllocation(
const FBoxSphereBounds& Bounds,
int32 BlockSize,
bool bPointSample,
bool bUnbuiltPreview,
FIndirectLightingCacheAllocation*& Allocation,
TMap<FIntVector, FBlockUpdateInfo>& BlocksToUpdate,
TArray<FIndirectLightingCacheAllocation*>& TransitionsOverTimeToUpdate)
{
bool bUpdated = false;
if (Allocation
&& Allocation->IsValid()
&& Allocation->AllocationTexelSize == BlockSize
&& Allocation->bPointSample == bPointSample)
{
FIndirectLightingCacheBlock& Block = FindBlock(Allocation->MinTexel);
// Calculate a potentially new min and size based on the current bounds
FVector NewMin;
FVector NewSize;
CalculateBlockPositionAndSize(Bounds, Block.TexelSize, NewMin, NewSize);
// If the primitive has moved enough to change its block min and size, we need to interpolate it again
if (Allocation->bIsDirty || GCacheUpdateEveryFrame || !Block.Min.Equals(NewMin) || !Block.Size.Equals(NewSize))
{
// Update the block and primitive allocation with the new bounds
Block.Min = NewMin;
Block.Size = NewSize;
FVector NewScale;
FVector NewAdd;
FVector MinUV;
FVector MaxUV;
CalculateBlockScaleAndAdd(Allocation->MinTexel, Allocation->AllocationTexelSize, NewMin, NewSize, NewScale, NewAdd, MinUV, MaxUV);
Allocation->SetParameters(Allocation->MinTexel, Allocation->AllocationTexelSize, NewScale, NewAdd, MinUV, MaxUV, bPointSample, bUnbuiltPreview);
BlocksToUpdate.Add(Block.MinTexel, FBlockUpdateInfo(Block, Allocation));
// Updating the block will also update the target position, meaning the ILC must be updated too.
TransitionsOverTimeToUpdate.AddUnique(Allocation);
bUpdated = true;
}
else if ((Allocation->SingleSamplePosition - Allocation->TargetPosition).SizeSquared() > DELTA)
{
TransitionsOverTimeToUpdate.AddUnique(Allocation);
bUpdated = true;
}
}
else
{
delete Allocation;
Allocation = CreateAllocation(BlockSize, Bounds, bPointSample, bUnbuiltPreview);
if (Allocation->IsValid())
{
// Must interpolate lighting for this new block
BlocksToUpdate.Add(Allocation->MinTexel, FBlockUpdateInfo(VolumeBlocks.FindChecked(Allocation->MinTexel), Allocation));
}
bUpdated = true;
}
return bUpdated;
}
void FIndirectLightingCache::UpdateCachePrimitive(
const TMap<FPrimitiveComponentId, FAttachmentGroupSceneInfo>& AttachmentGroups,
FPrimitiveSceneInfo* PrimitiveSceneInfo,
bool bAllowUnbuiltPreview,
bool bAllowVolumeSample,
TMap<FIntVector, FBlockUpdateInfo>& BlocksToUpdate,
TArray<FIndirectLightingCacheAllocation*>& TransitionsOverTimeToUpdate,
TArray<FPrimitiveSceneInfo*>& PrimitivesToUpdateStaticMeshes)
{
FPrimitiveSceneProxy* PrimitiveSceneProxy = PrimitiveSceneInfo->Proxy;
FIndirectLightingCacheAllocation** PrimitiveAllocationPtr = PrimitiveAllocations.Find(PrimitiveSceneInfo->PrimitiveComponentId);
FIndirectLightingCacheAllocation* PrimitiveAllocation = PrimitiveAllocationPtr != NULL ? *PrimitiveAllocationPtr : NULL;
const bool bIsMovable = PrimitiveSceneProxy->IsMovable();
if (PrimitiveSceneProxy->WillEverBeLit()
&& ((bAllowUnbuiltPreview && PrimitiveSceneProxy->HasStaticLighting() && PrimitiveAllocation && PrimitiveAllocation->bIsDirty)
|| (PrimitiveSceneProxy->IsMovable() && PrimitiveSceneProxy->GetIndirectLightingCacheQuality() != ILCQ_Off)))
{
const FIndirectLightingCacheAllocation* AttachmentParentAllocation = NULL;
if (PrimitiveSceneInfo->LightingAttachmentRoot.IsValid())
{
const FAttachmentGroupSceneInfo& AttachmentGroup = AttachmentGroups.FindChecked(PrimitiveSceneInfo->LightingAttachmentRoot);
if (AttachmentGroup.ParentSceneInfo && AttachmentGroup.ParentSceneInfo->Proxy->LightAttachmentsAsGroup())
{
AttachmentParentAllocation = FindPrimitiveAllocation(AttachmentGroup.ParentSceneInfo->PrimitiveComponentId);
}
}
const FIndirectLightingCacheAllocation* OriginalCachedIndirectLightingCacheAllocation = PrimitiveSceneInfo->IndirectLightingCacheAllocation;
if (AttachmentParentAllocation)
{
// Reuse the attachment parent's lighting allocation if part of an attachment group
PrimitiveSceneInfo->IndirectLightingCacheAllocation = AttachmentParentAllocation;
// Don't know here if this parent ILC is or will be dirty or not. Always update.
PrimitiveSceneInfo->MarkIndirectLightingCacheBufferDirty();
}
else
{
FIndirectLightingCacheAllocation* OriginalAllocation = PrimitiveAllocation;
const bool bUnbuiltPreview = bAllowUnbuiltPreview && !bIsMovable;
const bool bPointSample = PrimitiveSceneProxy->GetIndirectLightingCacheQuality() == ILCQ_Point || bUnbuiltPreview || !bAllowVolumeSample;
const int32 BlockSize = bPointSample ? 1 : GLightingCacheMovableObjectAllocationSize;
// Light with the cumulative bounds of the entire attachment group
const bool bUpdated = UpdateCacheAllocation(PrimitiveSceneInfo->GetAttachmentGroupBounds(), BlockSize, bPointSample, bUnbuiltPreview, PrimitiveAllocation, BlocksToUpdate, TransitionsOverTimeToUpdate);
// Cache the primitive allocation pointer on the FPrimitiveSceneInfo for base pass rendering
PrimitiveSceneInfo->IndirectLightingCacheAllocation = PrimitiveAllocation;
if (OriginalAllocation != PrimitiveAllocation)
{
if (OriginalAllocation)
{
PrimitiveAllocations.Remove(PrimitiveSceneInfo->PrimitiveComponentId);
}
// Allocate space in the atlas for this primitive and add it to a map, whose key is the component, so the allocation will persist through a re-register
PrimitiveAllocations.Add(PrimitiveSceneInfo->PrimitiveComponentId, PrimitiveAllocation);
}
if (bUpdated)
{
PrimitiveSceneInfo->MarkIndirectLightingCacheBufferDirty();
}
}
// Cached mesh draw command shader selection depends on IndirectLightingCacheAllocation existence and bPointSample value.
// If any of those changes, we need to re-cache mesh draw commands.
if (OriginalCachedIndirectLightingCacheAllocation == nullptr
|| (OriginalCachedIndirectLightingCacheAllocation
&& PrimitiveSceneInfo->IndirectLightingCacheAllocation
&& OriginalCachedIndirectLightingCacheAllocation->bPointSample != PrimitiveSceneInfo->IndirectLightingCacheAllocation->bPointSample))
{
PrimitivesToUpdateStaticMeshes.Add(PrimitiveSceneInfo);
}
}
}
void FIndirectLightingCache::UpdateBlocks(FScene* Scene, FViewInfo* DebugDrawingView, TMap<FIntVector, FBlockUpdateInfo>& BlocksToUpdate)
{
if (BlocksToUpdate.Num() > 0 && !IsInitialized())
{
InitResource(FRHICommandListImmediate::Get());
}
INC_DWORD_STAT_BY(STAT_IndirectLightingCacheUpdates, BlocksToUpdate.Num());
for (TMap<FIntVector, FBlockUpdateInfo>::TIterator It(BlocksToUpdate); It; ++It)
{
UpdateBlock(Scene, DebugDrawingView, It.Value());
}
}
void FIndirectLightingCache::UpdateTransitionsOverTime(const TArray<FIndirectLightingCacheAllocation*>& TransitionsOverTimeToUpdate, float DeltaWorldTime) const
{
for (int32 AllocationIndex = 0; AllocationIndex < TransitionsOverTimeToUpdate.Num(); AllocationIndex++)
{
FIndirectLightingCacheAllocation* Allocation = TransitionsOverTimeToUpdate[AllocationIndex];
const float TransitionDistance = (Allocation->SingleSamplePosition - Allocation->TargetPosition).Size();
if (TransitionDistance > DELTA)
{
// Transition faster for unbuilt meshes which is important for meshing visualization
const float EffectiveTransitionSpeed = GSingleSampleTransitionSpeed * (Allocation->bUnbuiltPreview ? 4 : 1);
const float LerpFactor = FMath::Clamp(GSingleSampleTransitionSpeed * DeltaWorldTime / TransitionDistance, 0.0f, 1.0f);
Allocation->SingleSamplePosition = FMath::Lerp(Allocation->SingleSamplePosition, Allocation->TargetPosition, LerpFactor);
for (int32 VectorIndex = 0; VectorIndex < 3; VectorIndex++) // RGB
{
Allocation->SingleSamplePacked0[VectorIndex] = FMath::Lerp(Allocation->SingleSamplePacked0[VectorIndex], Allocation->TargetSamplePacked0[VectorIndex], LerpFactor);
Allocation->SingleSamplePacked1[VectorIndex] = FMath::Lerp(Allocation->SingleSamplePacked1[VectorIndex], Allocation->TargetSamplePacked1[VectorIndex], LerpFactor);
}
Allocation->SingleSamplePacked2 = FMath::Lerp(Allocation->SingleSamplePacked2, Allocation->TargetSamplePacked2, LerpFactor);
Allocation->CurrentDirectionalShadowing = FMath::Lerp(Allocation->CurrentDirectionalShadowing, Allocation->TargetDirectionalShadowing, LerpFactor);
const FVector3f CurrentSkyBentNormal = FMath::Lerp(
FVector3f(Allocation->CurrentSkyBentNormal) * Allocation->CurrentSkyBentNormal.W,
FVector3f(Allocation->TargetSkyBentNormal) * Allocation->TargetSkyBentNormal.W,
LerpFactor);
const float BentNormalLength = CurrentSkyBentNormal.Size();
Allocation->CurrentSkyBentNormal = FVector4f(CurrentSkyBentNormal / FMath::Max(BentNormalLength, .0001f), BentNormalLength);
}
}
}
void FIndirectLightingCache::SetLightingCacheDirty(FScene* Scene, const FPrecomputedLightVolume* Volume)
{
if (Volume)
{
for (int32 PrimitiveIndex = 0; PrimitiveIndex < Scene->Primitives.Num(); ++PrimitiveIndex)
{
FPrimitiveSceneInfo* PrimitiveSceneInfo = Scene->Primitives[PrimitiveIndex];
FIndirectLightingCacheAllocation** PrimitiveAllocationPtr = PrimitiveAllocations.Find(PrimitiveSceneInfo->PrimitiveComponentId);
FIndirectLightingCacheAllocation* PrimitiveAllocation = PrimitiveAllocationPtr != NULL ? *PrimitiveAllocationPtr : NULL;
if (PrimitiveAllocation)
{
if (Volume->IntersectBounds(PrimitiveSceneInfo->Proxy->GetBounds()))
{
PrimitiveAllocation->bIsDirty = true;
PrimitiveAllocation->bHasEverUpdatedSingleSample = false;
}
}
}
}
else
{
for (TMap<FPrimitiveComponentId, FIndirectLightingCacheAllocation*>::TIterator It(PrimitiveAllocations); It; ++It)
{
It.Value()->bIsDirty = true;
It.Value()->bHasEverUpdatedSingleSample = false;
}
// next rendering we update all entries no matter if they are visible to avoid further hitches
bUpdateAllCacheEntries = true;
}
}
void FIndirectLightingCache::UpdateBlock(FScene* Scene, FViewInfo* DebugDrawingView, FBlockUpdateInfo& BlockInfo)
{
const int32 NumSamplesPerBlock = BlockInfo.Block.TexelSize * BlockInfo.Block.TexelSize * BlockInfo.Block.TexelSize;
FSHVectorRGB3 SingleSample;
float DirectionalShadowing = 1;
FVector SkyBentNormal(0, 0, 1);
//always do point interpolation to get valid 3band single sample and directional data.
InterpolatePoint(Scene, BlockInfo.Block, DirectionalShadowing, SingleSample, SkyBentNormal);
if (CanIndirectLightingCacheUseVolumeTexture(GetFeatureLevel()) && !BlockInfo.Allocation->bPointSample)
{
static TArray<float> AccumulatedWeight;
AccumulatedWeight.Reset(NumSamplesPerBlock);
AccumulatedWeight.AddZeroed(NumSamplesPerBlock);
//volume textures are encoded as two band, so no reason to waste perf interpolating 3 bands.
static TArray<FSHVectorRGB2> AccumulatedIncidentRadiance;
AccumulatedIncidentRadiance.Reset(NumSamplesPerBlock);
AccumulatedIncidentRadiance.AddZeroed(NumSamplesPerBlock);
// Interpolate SH samples from precomputed lighting samples and accumulate lighting data for an entire block
InterpolateBlock(Scene, BlockInfo.Block, AccumulatedWeight, AccumulatedIncidentRadiance);
static TArray<FFloat16Color> Texture0Data;
static TArray<FFloat16Color> Texture1Data;
static TArray<FFloat16Color> Texture2Data;
Texture0Data.Reset(NumSamplesPerBlock);
Texture1Data.Reset(NumSamplesPerBlock);
Texture2Data.Reset(NumSamplesPerBlock);
Texture0Data.AddUninitialized(NumSamplesPerBlock);
Texture1Data.AddUninitialized(NumSamplesPerBlock);
Texture2Data.AddUninitialized(NumSamplesPerBlock);
const int32 FormatSize = GPixelFormats[PF_FloatRGBA].BlockBytes;
check(FormatSize == sizeof(FFloat16Color));
// Encode the SH samples into a texture format
// Note the single sample is updated even if this is a volume allocation, because translucent materials only use the single sample
EncodeBlock(DebugDrawingView, BlockInfo.Block, AccumulatedWeight, AccumulatedIncidentRadiance, Texture0Data, Texture1Data, Texture2Data);
// Setup an update region
const FUpdateTextureRegion3D UpdateRegion(
BlockInfo.Block.MinTexel.X,
BlockInfo.Block.MinTexel.Y,
BlockInfo.Block.MinTexel.Z,
0,
0,
0,
BlockInfo.Block.TexelSize,
BlockInfo.Block.TexelSize,
BlockInfo.Block.TexelSize);
// Update the volume texture atlas
RHIUpdateTexture3D(GetTexture0(), 0, UpdateRegion, BlockInfo.Block.TexelSize * FormatSize, BlockInfo.Block.TexelSize * BlockInfo.Block.TexelSize * FormatSize, (const uint8*)Texture0Data.GetData());
RHIUpdateTexture3D(GetTexture1(), 0, UpdateRegion, BlockInfo.Block.TexelSize * FormatSize, BlockInfo.Block.TexelSize * BlockInfo.Block.TexelSize * FormatSize, (const uint8*)Texture1Data.GetData());
RHIUpdateTexture3D(GetTexture2(), 0, UpdateRegion, BlockInfo.Block.TexelSize * FormatSize, BlockInfo.Block.TexelSize * BlockInfo.Block.TexelSize * FormatSize, (const uint8*)Texture2Data.GetData());
}
else
{
if (GCacheDrawInterpolationPoints != 0 && DebugDrawingView)
{
FViewElementPDI DebugPDI(DebugDrawingView, nullptr, nullptr);
const FVector WorldPosition = BlockInfo.Block.Min;
DebugPDI.DrawPoint(WorldPosition, FLinearColor(0, 0, 1), 10, SDPG_World);
}
}
// Record the position that the sample was taken at
BlockInfo.Allocation->TargetPosition = BlockInfo.Block.Min + BlockInfo.Block.Size / 2;
BlockInfo.Allocation->TargetSamplePacked0[0] = FVector4f(SingleSample.R.V[0], SingleSample.R.V[1], SingleSample.R.V[2], SingleSample.R.V[3]) / PI;
BlockInfo.Allocation->TargetSamplePacked0[1] = FVector4f(SingleSample.G.V[0], SingleSample.G.V[1], SingleSample.G.V[2], SingleSample.G.V[3]) / PI;
BlockInfo.Allocation->TargetSamplePacked0[2] = FVector4f(SingleSample.B.V[0], SingleSample.B.V[1], SingleSample.B.V[2], SingleSample.B.V[3]) / PI;
BlockInfo.Allocation->TargetSamplePacked1[0] = FVector4f(SingleSample.R.V[4], SingleSample.R.V[5], SingleSample.R.V[6], SingleSample.R.V[7]) / PI;
BlockInfo.Allocation->TargetSamplePacked1[1] = FVector4f(SingleSample.G.V[4], SingleSample.G.V[5], SingleSample.G.V[6], SingleSample.G.V[7]) / PI;
BlockInfo.Allocation->TargetSamplePacked1[2] = FVector4f(SingleSample.B.V[4], SingleSample.B.V[5], SingleSample.B.V[6], SingleSample.B.V[7]) / PI;
BlockInfo.Allocation->TargetSamplePacked2 = FVector4f(SingleSample.R.V[8], SingleSample.G.V[8], SingleSample.B.V[8], 0) / PI;
BlockInfo.Allocation->TargetDirectionalShadowing = DirectionalShadowing;
const float BentNormalLength = SkyBentNormal.Size();
BlockInfo.Allocation->TargetSkyBentNormal = FVector4f(FVector3f(SkyBentNormal / FMath::Max(BentNormalLength, .0001f)), BentNormalLength);
if (!BlockInfo.Allocation->bHasEverUpdatedSingleSample)
{
// If this is the first update, also set the interpolated state to match the new target
//@todo - detect and handle teleports in the same way
BlockInfo.Allocation->SingleSamplePosition = BlockInfo.Allocation->TargetPosition;
for (int32 VectorIndex = 0; VectorIndex < 3; VectorIndex++) // RGB
{
BlockInfo.Allocation->SingleSamplePacked0[VectorIndex] = BlockInfo.Allocation->TargetSamplePacked0[VectorIndex];
BlockInfo.Allocation->SingleSamplePacked1[VectorIndex] = BlockInfo.Allocation->TargetSamplePacked1[VectorIndex];
}
BlockInfo.Allocation->SingleSamplePacked2 = BlockInfo.Allocation->TargetSamplePacked2;
BlockInfo.Allocation->CurrentDirectionalShadowing = BlockInfo.Allocation->TargetDirectionalShadowing;
BlockInfo.Allocation->CurrentSkyBentNormal = BlockInfo.Allocation->TargetSkyBentNormal;
BlockInfo.Allocation->bHasEverUpdatedSingleSample = true;
}
BlockInfo.Block.bHasEverBeenUpdated = true;
}
template class TSHVector<2>;
template class TSHVector<3>;
template <int32 SHOrder>
static void ReduceSHRinging(TSHVectorRGB<SHOrder>& IncidentRadiance)
{
const FVector BrightestDirection = IncidentRadiance.GetLuminance().GetMaximumDirection();
TSHVector<SHOrder> BrigthestDiffuseTransferSH = TSHVector<SHOrder>::CalcDiffuseTransfer(BrightestDirection);
FLinearColor BrightestLighting = Dot(IncidentRadiance, BrigthestDiffuseTransferSH);
TSHVector<SHOrder> OppositeDiffuseTransferSH = TSHVector<SHOrder>::CalcDiffuseTransfer(-BrightestDirection);
FLinearColor OppositeLighting = Dot(IncidentRadiance, OppositeDiffuseTransferSH);
// Try to maintain 5% of the brightest side on the opposite side
// This is necessary to reduce ringing artifacts when the SH contains mostly strong, directional lighting from one direction
FVector MinOppositeLighting = FVector(BrightestLighting) * .05f;
FVector NegativeAmount = (MinOppositeLighting - FVector(OppositeLighting)).ComponentMax(FVector(0));
//@todo - do this in a way that preserves energy and doesn't change hue
IncidentRadiance.AddAmbient(FLinearColor(NegativeAmount) * TSHVector<SHOrder>::ConstantBasisIntegral);
}
void FIndirectLightingCache::InterpolatePoint(
FScene* Scene,
const FIndirectLightingCacheBlock& Block,
float& OutDirectionalShadowing,
FSHVectorRGB3& OutIncidentRadiance,
FVector& OutSkyBentNormal)
{
FSHVectorRGB3 AccumulatedIncidentRadiance;
FVector AccumulatedSkyBentNormal(0, 0, 0);
float AccumulatedDirectionalShadowing = 0;
float AccumulatedWeight = 0;
for (int32 VolumeIndex = 0; VolumeIndex < Scene->PrecomputedLightVolumes.Num(); VolumeIndex++)
{
const FPrecomputedLightVolume* PrecomputedLightVolume = Scene->PrecomputedLightVolumes[VolumeIndex];
if (PrecomputedLightVolume)
{
PrecomputedLightVolume->InterpolateIncidentRadiancePoint(
Block.Min + Block.Size / 2,
AccumulatedWeight,
AccumulatedDirectionalShadowing,
AccumulatedIncidentRadiance,
AccumulatedSkyBentNormal);
}
}
if (AccumulatedWeight > 0)
{
OutDirectionalShadowing = AccumulatedDirectionalShadowing / AccumulatedWeight;
OutIncidentRadiance = AccumulatedIncidentRadiance / AccumulatedWeight;
OutSkyBentNormal = AccumulatedSkyBentNormal / AccumulatedWeight;
if (GCacheReduceSHRinging != 0)
{
ReduceSHRinging(OutIncidentRadiance);
}
}
else
{
OutIncidentRadiance = AccumulatedIncidentRadiance;
OutDirectionalShadowing = 1;
// Use an unoccluded vector if no valid samples were found for interpolation
OutSkyBentNormal = FVector(0, 0, 1);
}
}
void FIndirectLightingCache::InterpolateBlock(
FScene* Scene,
const FIndirectLightingCacheBlock& Block,
TArray<float>& AccumulatedWeight,
TArray<FSHVectorRGB2>& AccumulatedIncidentRadiance)
{
const FBoxCenterAndExtent BlockBoundingBox(Block.Min + Block.Size / 2, Block.Size / 2);
const FVector HalfTexelWorldOffset = BlockBoundingBox.Extent / FVector(Block.TexelSize);
if (GCacheLimitQuerySize && Block.TexelSize > 2)
{
for (int32 VolumeIndex = 0; VolumeIndex < Scene->PrecomputedLightVolumes.Num(); VolumeIndex++)
{
const FPrecomputedLightVolume* PrecomputedLightVolume = Scene->PrecomputedLightVolumes[VolumeIndex];
// Compute the target query size
// We will try to split up the allocation into groups that are smaller than this before querying the octree
// This prevents very large objects from finding all the samples in the level in their octree search
const float WorldTargetSize = PrecomputedLightVolume->GetNodeLevelExtent(GCacheQueryNodeLevel) * 2;
const FVector WorldCellSize = Block.Size / FVector(Block.TexelSize);
// Number of cells to increment by for query blocks
FIntVector NumStepCells;
NumStepCells.X = FMath::Max(1, FMath::FloorToInt(WorldTargetSize / WorldCellSize.X));
NumStepCells.Y = FMath::Max(1, FMath::FloorToInt(WorldTargetSize / WorldCellSize.Y));
NumStepCells.Z = FMath::Max(1, FMath::FloorToInt(WorldTargetSize / WorldCellSize.Z));
FIntVector NumQueryStepCells(0, 0, 0);
// World space size to increment by for query blocks
const FVector WorldStepSize = FVector(NumStepCells) * WorldCellSize;
FVector QueryWorldStepSize(0, 0, 0);
check(NumStepCells.X > 0 && NumStepCells.Y > 0 && NumStepCells.Z > 0);
// This will track the position in cells of the query block being built
FIntVector CellIndex(0, 0, 0);
// This will track the min world position of the query block being built
FVector MinPosition = Block.Min;
for (MinPosition.Z = Block.Min.Z, CellIndex.Z = 0;
CellIndex.Z < Block.TexelSize;
MinPosition.Z += WorldStepSize.Z, CellIndex.Z += NumStepCells.Z)
{
QueryWorldStepSize.Z = WorldStepSize.Z;
NumQueryStepCells.Z = NumStepCells.Z;
// If this is the last query block in this dimension, adjust both the world space and cell sizes to match
if (CellIndex.Z + NumStepCells.Z > Block.TexelSize)
{
QueryWorldStepSize.Z = Block.Min.Z + Block.Size.Z - MinPosition.Z;
NumQueryStepCells.Z = Block.TexelSize - CellIndex.Z;
}
for (MinPosition.Y = Block.Min.Y, CellIndex.Y = 0;
CellIndex.Y < Block.TexelSize;
MinPosition.Y += WorldStepSize.Y, CellIndex.Y += NumStepCells.Y)
{
QueryWorldStepSize.Y = WorldStepSize.Y;
NumQueryStepCells.Y = NumStepCells.Y;
if (CellIndex.Y + NumStepCells.Y > Block.TexelSize)
{
QueryWorldStepSize.Y = Block.Min.Y + Block.Size.Y - MinPosition.Y;
NumQueryStepCells.Y = Block.TexelSize - CellIndex.Y;
}
for (MinPosition.X = Block.Min.X, CellIndex.X = 0;
CellIndex.X < Block.TexelSize;
MinPosition.X += WorldStepSize.X, CellIndex.X += NumStepCells.X)
{
QueryWorldStepSize.X = WorldStepSize.X;
NumQueryStepCells.X = NumStepCells.X;
if (CellIndex.X + NumStepCells.X > Block.TexelSize)
{
QueryWorldStepSize.X = Block.Min.X + Block.Size.X - MinPosition.X;
NumQueryStepCells.X = Block.TexelSize - CellIndex.X;
}
FVector BoxExtent = QueryWorldStepSize / 2;
// Use a 0 query extent in dimensions that only have one cell, these become point queries
BoxExtent.X = NumQueryStepCells.X == 1 ? 0 : BoxExtent.X;
BoxExtent.Y = NumQueryStepCells.Y == 1 ? 0 : BoxExtent.Y;
BoxExtent.Z = NumQueryStepCells.Z == 1 ? 0 : BoxExtent.Z;
// Build a bounding box for the query block
const FBoxCenterAndExtent BoundingBox(MinPosition + BoxExtent + HalfTexelWorldOffset, BoxExtent);
checkSlow(CellIndex.X < Block.TexelSize && CellIndex.Y < Block.TexelSize && CellIndex.Z < Block.TexelSize);
checkSlow(CellIndex.X + NumQueryStepCells.X <= Block.TexelSize
&& CellIndex.Y + NumQueryStepCells.Y <= Block.TexelSize
&& CellIndex.Z + NumQueryStepCells.Z <= Block.TexelSize);
// Interpolate from the SH volume lighting samples that Lightmass computed
PrecomputedLightVolume->InterpolateIncidentRadianceBlock(
BoundingBox,
NumQueryStepCells,
FIntVector(Block.TexelSize),
CellIndex,
AccumulatedWeight,
AccumulatedIncidentRadiance);
}
}
}
}
}
else
{
for (int32 VolumeIndex = 0; VolumeIndex < Scene->PrecomputedLightVolumes.Num(); VolumeIndex++)
{
const FPrecomputedLightVolume* PrecomputedLightVolume = Scene->PrecomputedLightVolumes[VolumeIndex];
check(PrecomputedLightVolume);
check(PrecomputedLightVolume->IsUsingHighQualityLightMap() == AllowHighQualityLightmaps(Scene->GetFeatureLevel()));
// Interpolate from the SH volume lighting samples that Lightmass computed
// Query using the bounds of all the samples in this block
// There will be a performance cliff for large objects which end up intersecting with the entire octree
PrecomputedLightVolume->InterpolateIncidentRadianceBlock(
FBoxCenterAndExtent(BlockBoundingBox.Center + HalfTexelWorldOffset, BlockBoundingBox.Extent),
FIntVector(Block.TexelSize),
FIntVector(Block.TexelSize),
FIntVector(0),
AccumulatedWeight,
AccumulatedIncidentRadiance);
}
}
}
void FIndirectLightingCache::EncodeBlock(
FViewInfo* DebugDrawingView,
const FIndirectLightingCacheBlock& Block,
const TArray<float>& AccumulatedWeight,
const TArray<FSHVectorRGB2>& AccumulatedIncidentRadiance,
TArray<FFloat16Color>& Texture0Data,
TArray<FFloat16Color>& Texture1Data,
TArray<FFloat16Color>& Texture2Data
)
{
FViewElementPDI DebugPDI(DebugDrawingView, nullptr, nullptr);
for (int32 Z = 0; Z < Block.TexelSize; Z++)
{
for (int32 Y = 0; Y < Block.TexelSize; Y++)
{
for (int32 X = 0; X < Block.TexelSize; X++)
{
const int32 LinearIndex = Z * Block.TexelSize * Block.TexelSize + Y * Block.TexelSize + X;
FSHVectorRGB2 IncidentRadiance = AccumulatedIncidentRadiance[LinearIndex];
float Weight = AccumulatedWeight[LinearIndex];
if (Weight > 0)
{
IncidentRadiance = IncidentRadiance / Weight;
if (GCacheReduceSHRinging != 0)
{
ReduceSHRinging(IncidentRadiance);
}
}
if (GCacheDrawInterpolationPoints != 0 && DebugDrawingView)
{
const FVector WorldPosition = Block.Min + (FVector(X, Y, Z) + .5f) / Block.TexelSize * Block.Size;
DebugPDI.DrawPoint(WorldPosition, FLinearColor(0, 0, 1), 10, SDPG_World);
}
Texture0Data[LinearIndex] = FLinearColor(IncidentRadiance.R.V[0], IncidentRadiance.G.V[0], IncidentRadiance.B.V[0], IncidentRadiance.R.V[3]);
Texture1Data[LinearIndex] = FLinearColor(IncidentRadiance.R.V[1], IncidentRadiance.G.V[1], IncidentRadiance.B.V[1], IncidentRadiance.G.V[3]);
Texture2Data[LinearIndex] = FLinearColor(IncidentRadiance.R.V[2], IncidentRadiance.G.V[2], IncidentRadiance.B.V[2], IncidentRadiance.B.V[3]);
}
}
}
}