// 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(FMath::RoundToInt(NormalizedTheta * (MAX_uint8 - 1)), 0, MAX_uint8 - 1); QuantizedPhi = (uint8)FMath::Clamp(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 PreviousIncidentRadiances; TArray 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 TLightingCache : public FLightingCacheBase { public: /** The irradiance for a single static lighting vertex. */ template 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,FRecordOctreeSemantics> LightingOctreeType; /** The octree semantics for irradiance records. */ struct FRecordOctreeSemantics { enum { MaxElementsPerLeaf = 4 }; enum { MaxNodeDepth = 24 }; enum { LoosenessDenominator = 16 }; typedef TInlineAllocator ElementAllocator; static FBoxCenterAndExtent GetBoundingBox(const FRecord& 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& Record, bool bInsideGeometry, bool bAddToStats) { Record.Id = NextRecordId; NextRecordId++; Octree.AddElement(Record); if (bAddToStats) { Stats.NumRecords++; if (bInsideGeometry) { Stats.NumInsideGeometry++; } } } void GetAllRecords(TArray >& 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& 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& 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 Ranges; TArray 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 bool TLightingCache::InterpolateLighting( const FFullStaticLightingVertex& Vertex, bool bFirstPass, bool bDebugThisSample, float SecondInterpolationSmoothnessReduction, SampleType& OutLighting, SampleType& OutSecondLighting, float& OutBackfacingHitsFraction, TArray& 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& 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