Files
UnrealEngine/Engine/Source/Programs/UnrealLightmass/Private/Lighting/LightingCache.h
2025-05-18 13:04:45 +08:00

520 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "LightingMesh.h"
#include "LMOctree.h"
namespace Lightmass
{
class FIrradianceCacheStats
{
public:
uint64 NumCacheLookups;
uint64 NumRecords;
uint64 NumInsideGeometry;
FIrradianceCacheStats() :
NumCacheLookups(0),
NumRecords(0),
NumInsideGeometry(0)
{}
FIrradianceCacheStats& operator+=(const FIrradianceCacheStats& B)
{
NumCacheLookups += B.NumCacheLookups;
NumRecords += B.NumRecords;
NumInsideGeometry += B.NumInsideGeometry;
return *this;
}
};
// max absolute error 9.0x10^-3
// Eberly's polynomial degree 1 - respect bounds
// input [-1, 1] and output [0, PI]
inline float acosFast(float inX)
{
float x = FMath::Abs(inX);
float res = -0.156583f * x + (0.5 * PI);
res *= sqrt(1.0f - x);
return (inX >= 0) ? res : PI - res;
}
class FQuantizedHemisphereDirection
{
public:
FQuantizedHemisphereDirection() :
QuantizedTheta(0),
QuantizedPhi(0)
{}
FQuantizedHemisphereDirection(const FVector4f& UnitTangentSpaceDirection)
{
checkSlow(UnitTangentSpaceDirection.IsUnit3());
checkSlow(UnitTangentSpaceDirection.Z >= 0 && UnitTangentSpaceDirection.Z <= 1);
const float ThetaFloat = acosFast(UnitTangentSpaceDirection.Z);
const float PhiFloat = FMath::Atan2(UnitTangentSpaceDirection.Y, UnitTangentSpaceDirection.X);
const float NormalizedTheta = ThetaFloat / PI;
const float NormalizedPhi = PhiFloat / (2 * PI) + .5f;
QuantizedTheta = (uint8)FMath::Clamp<int32>(FMath::RoundToInt(NormalizedTheta * (MAX_uint8 - 1)), 0, MAX_uint8 - 1);
QuantizedPhi = (uint8)FMath::Clamp<int32>(FMath::RoundToInt(NormalizedPhi * (MAX_uint8 - 1)), 0, MAX_uint8 - 1);
}
FVector4f GetDirection() const
{
const float Uint8RangeScale = 1.0f / (float)(MAX_uint8 - 1);
const float RescaledTheta = QuantizedTheta * Uint8RangeScale * PI;
const float RescaledPhi = (QuantizedPhi * Uint8RangeScale - .5f) * 2 * PI;
const float SinTheta = FMath::Sin(RescaledTheta);
return FVector3f(SinTheta * FMath::Cos(RescaledPhi), SinTheta * FMath::Sin(RescaledPhi), FMath::Cos(RescaledTheta));
}
private:
uint8 QuantizedTheta;
uint8 QuantizedPhi;
};
class FFinalGatherHitPoint
{
public:
FFinalGatherHitPoint() :
MappingIndex(-1),
MappingSurfaceCoordinate(-1),
Weight(0.0f)
{}
int32 MappingIndex;
int32 MappingSurfaceCoordinate;
FFloat16 Weight;
};
/** The information needed by the lighting cache from a uniform sampled integration of the hemisphere in order to create a lighting record at that point. */
class FLightingCacheGatherInfo
{
public:
float MinDistance;
float BackfacingHitsFraction;
/** Incident radiance and distance from each hemisphere sample. */
TArray<FLinearColor> PreviousIncidentRadiances;
TArray<float> PreviousDistances;
class FGatherHitPoints* HitPointRecorder;
FLightingCacheGatherInfo() :
MinDistance(FLT_MAX),
BackfacingHitsFraction(0),
HitPointRecorder(NULL)
{}
inline void UpdateOnHit(float IntersectionDistance)
{
MinDistance = FMath::Min(MinDistance, IntersectionDistance);
}
};
class FLightingCacheBase
{
public:
/** See FIrradianceCachingSettings for descriptions of these or the variables they are based on. */
float InterpolationAngleNormalization;
float InterpolationAngleNormalizationSmooth;
const float MinCosPointBehindPlane;
const float DistanceSmoothFactor;
const bool bUseIrradianceGradients;
const bool bShowGradientsOnly;
const bool bVisualizeIrradianceSamples;
const int32 BounceNumber;
int32 NextRecordId;
mutable FIrradianceCacheStats Stats;
const class FStaticLightingSystem& System;
/** Initialization constructor. */
FLightingCacheBase(const FStaticLightingSystem& InSystem, int32 InBounceNumber);
};
/** A lighting cache. */
template<class SampleType>
class TLightingCache : public FLightingCacheBase
{
public:
/** The irradiance for a single static lighting vertex. */
template<class RecordSampleType>
class FRecord
{
public:
/** The static lighting vertex the irradiance record was computed for. */
FFullStaticLightingVertex Vertex;
int32 ElementIndex;
/** Largest radius that the sample will ever have, used for insertion into spatial data structures. */
float BoundingRadius;
/** Radius of this irradiance cache record in the cache pass. */
float Radius;
/** Radius of this irradiance cache record in the interpolation pass. */
float InterpolationRadius;
/** The lighting incident on an infinitely small surface at WorldPosition facing along WorldNormal. */
RecordSampleType Lighting;
/** The rotational gradient along the vector perpendicular to both the record normal and the normal of the vertex being interpolated to, used for higher order interpolation. */
FVector4f RotationalGradient;
/** The translational gradient from the record to the point being interpolated to, used for higher order interpolation. */
FVector4f TranslationalGradient;
/** For debugging */
int32 Id;
float BackfacingHitsFraction;
/** Initialization constructor. */
FRecord(const FFullStaticLightingVertex& InVertex,int32 InElementIndex,const FLightingCacheGatherInfo& GatherInfo,float SampleRadius,float InOverrideRadius,const FIrradianceCachingSettings& IrradianceCachingSettings,const FStaticLightingSettings& GeneralSettings,const RecordSampleType& InLighting, const FVector4f& InRotGradient, const FVector4f& InTransGradient):
Vertex(InVertex),
ElementIndex(InElementIndex),
Lighting(InLighting),
RotationalGradient(InRotGradient),
TranslationalGradient(InTransGradient),
Id(-1)
{
// Clamp to be larger than the texel
Radius = FMath::Clamp(GatherInfo.MinDistance, SampleRadius, IrradianceCachingSettings.MaxRecordRadius) * IrradianceCachingSettings.RecordRadiusScale;
// Use a larger radius to interpolate, which smooths the error
InterpolationRadius = Radius * FMath::Max(IrradianceCachingSettings.DistanceSmoothFactor * GeneralSettings.IndirectLightingSmoothness, 1.0f);
BoundingRadius = FMath::Max(Radius, InterpolationRadius);
BackfacingHitsFraction = GatherInfo.BackfacingHitsFraction;
}
};
struct FRecordOctreeSemantics;
/** The type of lighting cache octree nodes. */
typedef TOctree<FRecord<SampleType>,FRecordOctreeSemantics> LightingOctreeType;
/** The octree semantics for irradiance records. */
struct FRecordOctreeSemantics
{
enum { MaxElementsPerLeaf = 4 };
enum { MaxNodeDepth = 24 };
enum { LoosenessDenominator = 16 };
typedef TInlineAllocator<MaxElementsPerLeaf * 8> ElementAllocator;
static FBoxCenterAndExtent GetBoundingBox(const FRecord<SampleType>& LightingRecord)
{
return FBoxCenterAndExtent(
LightingRecord.Vertex.WorldPosition,
FVector4f(LightingRecord.BoundingRadius, LightingRecord.BoundingRadius, LightingRecord.BoundingRadius)
);
}
};
TLightingCache(const FBox3f& InBoundingBox, const FStaticLightingSystem& System, int32 InBounceNumber) :
FLightingCacheBase(System, InBounceNumber),
Octree(InBoundingBox.GetCenter(),InBoundingBox.GetExtent().GetMax())
{}
/** Adds a lighting record to the cache. */
void AddRecord(FRecord<SampleType>& Record, bool bInsideGeometry, bool bAddToStats)
{
Record.Id = NextRecordId;
NextRecordId++;
Octree.AddElement(Record);
if (bAddToStats)
{
Stats.NumRecords++;
if (bInsideGeometry)
{
Stats.NumInsideGeometry++;
}
}
}
void GetAllRecords(TArray<FRecord<SampleType> >& Records) const
{
// Gather an array of samples from the octree
for(typename LightingOctreeType::template TConstIterator<> NodeIt(Octree); NodeIt.HasPendingNodes(); NodeIt.Advance())
{
const typename LightingOctreeType::FNode& CurrentNode = NodeIt.GetCurrentNode();
FOREACH_OCTREE_CHILD_NODE(ChildRef)
{
if(CurrentNode.HasChild(ChildRef))
{
NodeIt.PushChild(ChildRef);
}
}
for (typename LightingOctreeType::ElementConstIt ElementIt(CurrentNode.GetConstElementIt()); ElementIt; ++ElementIt)
{
const FRecord<SampleType>& Sample = *ElementIt;
Records.Add(Sample);
}
}
}
LightingOctreeType& GetOctree() { return Octree; }
/**
* Interpolates nearby lighting records for a vertex.
* @param Vertex - The vertex to interpolate the lighting for.
* @param OutLighting - If true is returned, contains the blended lighting records that were found near the point.
* @return true if nearby records were found with enough relevance to interpolate this point's lighting.
*/
bool InterpolateLighting(
const FFullStaticLightingVertex& Vertex,
bool bFirstPass,
bool bDebugThisSample,
float SecondInterpolationSmoothnessReduction,
SampleType& OutLighting,
SampleType& OutSecondLighting,
float& OutBackfacingHitsFraction,
TArray<FDebugLightingCacheRecord>& DebugCacheRecords,
class FInfluencingRecordCollector* RecordCollector = NULL) const;
private:
/** The lighting cache octree. */
LightingOctreeType Octree;
};
class FInfluencingRecord
{
public:
int32 RecordIndex;
FFloat16 RecordWeight;
FInfluencingRecord(int32 InRecordIndex, FFloat16 InRecordWeight) :
RecordIndex(InRecordIndex),
RecordWeight(InRecordWeight)
{}
};
class FArrayRange
{
public:
FArrayRange(int32 InStartIndex) :
StartIndex(InStartIndex),
NumEntries(0)
{}
int32 StartIndex;
int32 NumEntries;
};
class FInfluencingRecords
{
public:
TArray<FArrayRange> Ranges;
TArray<FInfluencingRecord> Data;
size_t GetAllocatedSize() const { return Ranges.GetAllocatedSize() + Data.GetAllocatedSize(); }
};
class FInfluencingRecordCollector
{
public:
FInfluencingRecordCollector(FInfluencingRecords& InInfluencingRecords, int32 InCurrentRangeIndex) :
CurrentRangeIndex(InCurrentRangeIndex),
InfluencingRecords(InInfluencingRecords)
{}
void AddInfluencingRecord(int32 RecordId, float Weight)
{
InfluencingRecords.Ranges[CurrentRangeIndex].NumEntries++;
InfluencingRecords.Data.Add(FInfluencingRecord(RecordId, Weight));
}
int32 CurrentRangeIndex;
FInfluencingRecords& InfluencingRecords;
};
/**
* Interpolates nearby lighting records for a vertex.
* @param Vertex - The vertex to interpolate the lighting for.
* @param OutLighting - If true is returned, contains the blended lighting records that were found near the point.
* @return true if nearby records were found with enough relevance to interpolate this point's lighting.
*/
template<class SampleType>
bool TLightingCache<SampleType>::InterpolateLighting(
const FFullStaticLightingVertex& Vertex,
bool bFirstPass,
bool bDebugThisSample,
float SecondInterpolationSmoothnessReduction,
SampleType& OutLighting,
SampleType& OutSecondLighting,
float& OutBackfacingHitsFraction,
TArray<FDebugLightingCacheRecord>& DebugCacheRecords,
FInfluencingRecordCollector* RecordCollector) const
{
if (bFirstPass)
{
Stats.NumCacheLookups++;
}
const float AngleNormalization = bFirstPass ? InterpolationAngleNormalization : InterpolationAngleNormalizationSmooth;
// Initialize the sample to zero
SampleType AccumulatedLighting(ForceInit);
float TotalWeight = 0.0f;
SampleType SecondAccumulatedLighting(ForceInit);
float SecondTotalWeight = 0.0f;
float AccumulatedBackfacingHitsFraction = 0.0f;
// Iterate over the octree nodes containing the query point.
for( typename LightingOctreeType::template TConstElementBoxIterator<> OctreeIt(
Octree,
FBoxCenterAndExtent(Vertex.WorldPosition, FVector4f(0,0,0))
);
OctreeIt.HasPendingElements();
OctreeIt.Advance())
{
const FRecord<SampleType>& LightingRecord = OctreeIt.GetCurrentElement();
// Check whether the query point is farther than the record's intersection distance for the direction to the query point.
const float DistanceSquared = (LightingRecord.Vertex.WorldPosition - Vertex.WorldPosition).SizeSquared3();
if (DistanceSquared > FMath::Square(LightingRecord.BoundingRadius))
{
continue;
}
const float Distance = FMath::Sqrt(DistanceSquared);
// Don't use a lighting record if it's in front of the query point.
// Query points behind the lighting record may have nearby occluders that the lighting record does not see.
const FVector4f RecordToVertexVector = Vertex.WorldPosition - LightingRecord.Vertex.WorldPosition;
// Use the average normal to handle surfaces with constant concavity
const FVector4f AverageNormal = (LightingRecord.Vertex.TriangleNormal + Vertex.TriangleNormal).GetSafeNormal();
const float PlaneDistance = Dot3(AverageNormal, RecordToVertexVector.GetSafeNormal());
// Setup an error metric that goes from 0 if the points are coplanar, to 1 if the point being shaded is at the angle corresponding to MinCosPointBehindPlane behind the plane
const float PointBehindPlaneError = FMath::Max(PlaneDistance / MinCosPointBehindPlane, 0.0f);
const float NormalDot = Dot3(LightingRecord.Vertex.WorldTangentZ, Vertex.WorldTangentZ);
const float NonGradientLighting = bShowGradientsOnly ? 0.0f : 1.0f;
float RotationalGradientContribution = 0.0f;
float TranslationalGradientContribution = 0.0f;
if (bUseIrradianceGradients)
{
// Calculate the gradient's contribution
RotationalGradientContribution = Dot3((LightingRecord.Vertex.WorldTangentZ ^ Vertex.WorldTangentZ), LightingRecord.RotationalGradient);
TranslationalGradientContribution = Dot3((Vertex.WorldPosition - LightingRecord.Vertex.WorldPosition), LightingRecord.TranslationalGradient);
}
// Error metric from "An Approximate Global Illumination System for Computer Generated Films",
// This error metric has the advantages (over Ward's original metric from "A Ray Tracing Solution to Diffuse Interreflection")
// That it goes to 0 at the record's radius, which avoids discontinuities,
// And it is finite at the record's center, which allows filtering the records to be more effective.
const float EffectiveRadius = bFirstPass ? LightingRecord.Radius : LightingRecord.InterpolationRadius;
{
const float DistanceRatio = Distance / EffectiveRadius;
const float NormalRatio = AngleNormalization * FMath::Sqrt(FMath::Max(1.0f - NormalDot, 0.0f));
// The total error is the max of the distance, normal and plane errors
float RecordError = FMath::Max(DistanceRatio, NormalRatio);
RecordError = FMath::Max(RecordError, PointBehindPlaneError);
if (RecordError < 1)
{
const float RecordWeight = 1.0f - RecordError;
//@todo - Rotate the record's lighting into this vertex's tangent basis. We are linearly combining incident lighting in different coordinate spaces.
AccumulatedLighting = AccumulatedLighting + LightingRecord.Lighting * RecordWeight * (NonGradientLighting + RotationalGradientContribution + TranslationalGradientContribution);
AccumulatedBackfacingHitsFraction += LightingRecord.BackfacingHitsFraction * RecordWeight * (NonGradientLighting + RotationalGradientContribution + TranslationalGradientContribution);
// Accumulate the weight of all records
TotalWeight += RecordWeight;
if (RecordCollector)
{
RecordCollector->AddInfluencingRecord(LightingRecord.Id, RecordWeight);
}
if (bVisualizeIrradianceSamples && bDebugThisSample && BounceNumber == 1)
{
for (int32 i = 0; i < DebugCacheRecords.Num(); i++)
{
FDebugLightingCacheRecord& CurrentRecord = DebugCacheRecords[i];
if (CurrentRecord.RecordId == LightingRecord.Id)
{
CurrentRecord.bAffectsSelectedTexel = true;
}
}
}
}
}
// Accumulate a second interpolation with reduced smoothness
// This is useful for lighting components like AO and sky shadowing where less smoothing is needed to hide noise
// This interpolation is done in the same pass to prevent another traversal of the octree
{
const float DistanceRatio = Distance / FMath::Lerp(LightingRecord.Radius, EffectiveRadius, SecondInterpolationSmoothnessReduction);
const float SecondAngleNormalization = FMath::Lerp(InterpolationAngleNormalization, AngleNormalization, SecondInterpolationSmoothnessReduction);
const float NormalRatio = SecondAngleNormalization * FMath::Sqrt(FMath::Max(1.0f - NormalDot, 0.0f));
// The total error is the max of the distance, normal and plane errors
float RecordError = FMath::Max(DistanceRatio, NormalRatio);
RecordError = FMath::Max(RecordError, PointBehindPlaneError);
if (RecordError < 1)
{
const float RecordWeight = 1.0f - RecordError;
//@todo - Rotate the record's lighting into this vertex's tangent basis. We are linearly combining incident lighting in different coordinate spaces.
SecondAccumulatedLighting = SecondAccumulatedLighting + LightingRecord.Lighting * RecordWeight * (NonGradientLighting + RotationalGradientContribution + TranslationalGradientContribution);
// Accumulate the weight of all records
SecondTotalWeight += RecordWeight;
}
}
if (bVisualizeIrradianceSamples && bDebugThisSample && BounceNumber == 1)
{
for (int32 i = 0; i < DebugCacheRecords.Num(); i++)
{
FDebugLightingCacheRecord& CurrentRecord = DebugCacheRecords[i];
if (CurrentRecord.RecordId == LightingRecord.Id)
{
CurrentRecord.bAffectsSelectedTexel = true;
}
}
}
}
if (TotalWeight > DELTA)
{
// Normalize the accumulated lighting and return success.
const float InvTotalWeight = 1.0f / TotalWeight;
OutLighting = OutLighting + AccumulatedLighting * InvTotalWeight;
OutSecondLighting = OutSecondLighting + SecondAccumulatedLighting * (1.0f / SecondTotalWeight);
OutBackfacingHitsFraction = AccumulatedBackfacingHitsFraction * InvTotalWeight;
return true;
}
else
{
// Irradiance for the query vertex couldn't be interpolated from the cache
return false;
}
}
} //namespace Lightmass