// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "LightingSystem.h" #include "HAL/RunnableThread.h" #include "MonteCarlo.h" #include "LightmassSwarm.h" #include "HAL/ExceptionHandling.h" namespace Lightmass { /** Average fraction of emitted direct photons that get deposited on surfaces, used to presize gathered photon arrays. */ static const float DirectPhotonEfficiency = .3f; /** Average fraction of emitted indirect photons that get deposited on surfaces, used to presize gathered photon arrays. */ static const float IndirectPhotonEfficiency = .1f; // Number of parts that photon operating passes will be split into. // Random number generators and other state will be seeded at the beginning of each work range, // To ensure deterministic behavior regardless of how many threads are processing and what order operations happen within each thread. static const int32 NumPhotonWorkRanges = 256; /** Sets up photon mapping settings. */ void FStaticLightingSystem::InitializePhotonSettings() { const FBoxSphereBounds3f SceneBounds = FBoxSphereBounds3f(AggregateMesh->GetBounds()); const FBoxSphereBounds3f ImportanceBounds = GetImportanceBounds(); // Get direct photon counts from each light #if !LIGHTMASS_DO_PROCESSING const int32 MaxNumDirectPhotonsToEmit = 10; #else // Maximum number of direct photons to emit, used to cap the memory and processing time for a given build //@todo - remove these clamps and come up with a robust solution for huge scenes const int32 MaxNumDirectPhotonsToEmit = 40000000; #endif NumDirectPhotonsToEmit = 0; Stats.NumFirstPassPhotonsRequested = 0; // Only emit direct photons if they will be used for lighting if (GeneralSettings.NumIndirectLightingBounces > 0 || PhotonMappingSettings.bVisualizeCachedApproximateDirectLighting) { for (int32 LightIndex = 0; LightIndex < Lights.Num(); LightIndex++) { Stats.NumFirstPassPhotonsRequested += Lights[LightIndex]->GetNumDirectPhotons(PhotonMappingSettings.DirectPhotonDensity); } NumDirectPhotonsToEmit = FMath::Min(Stats.NumFirstPassPhotonsRequested, (uint64)MaxNumDirectPhotonsToEmit); if (NumDirectPhotonsToEmit == MaxNumDirectPhotonsToEmit) { LogSolverMessage(FString::Printf(TEXT("Clamped the number of direct photons to emit to %.3f million, from %.3f million requested."), MaxNumDirectPhotonsToEmit / 1000000.0f, Stats.NumFirstPassPhotonsRequested / 1000000.0f)); } } DirectIrradiancePhotonFraction = FMath::Clamp(Scene.PhotonMappingSettings.DirectIrradiancePhotonDensity / Scene.PhotonMappingSettings.DirectPhotonDensity, 0.0f, 1.0f); // Calculate numbers of photons to gather based on the scene using the given photon densities, the scene's surface area and the importance volume's surface area float SceneSurfaceAreaMillionUnits = FMath::Max(AggregateMesh->GetSurfaceArea() / 1000000.0f, DELTA); float SceneSurfaceAreaMillionUnitsEstimate = FMath::Max(4.0f * (float)PI * SceneBounds.SphereRadius * SceneBounds.SphereRadius / 1000000.0f, DELTA); float SceneSurfaceAreaMillionUnitsEstimateDiff = SceneSurfaceAreaMillionUnitsEstimate > DELTA ? ( SceneSurfaceAreaMillionUnits / SceneSurfaceAreaMillionUnitsEstimate * 100.0f ) : 0.0f; LogSolverMessage(FString::Printf(TEXT("Scene surface area calculated at %.3f million units (%.3f%% of the estimated %.3f million units)"), SceneSurfaceAreaMillionUnits, SceneSurfaceAreaMillionUnitsEstimateDiff, SceneSurfaceAreaMillionUnitsEstimate)); float ImportanceSurfaceAreaMillionUnits = FMath::Max(AggregateMesh->GetSurfaceAreaWithinImportanceVolume() / 1000000.0f, DELTA); float ImportanceSurfaceAreaMillionUnitsEstimate = FMath::Max(4.0f * (float)PI * ImportanceBounds.SphereRadius * ImportanceBounds.SphereRadius / 1000000.0f, DELTA); float ImportanceSurfaceAreaMillionUnitsEstimateDiff = ImportanceSurfaceAreaMillionUnitsEstimate > DELTA ? ( ImportanceSurfaceAreaMillionUnits / ImportanceSurfaceAreaMillionUnitsEstimate * 100.0f ) : 0.0f; LogSolverMessage(FString::Printf(TEXT("Importance volume surface area calculated at %.3f million units (%.3f%% of the estimated %.3f million units)"), ImportanceSurfaceAreaMillionUnits, ImportanceSurfaceAreaMillionUnitsEstimateDiff, ImportanceSurfaceAreaMillionUnitsEstimate)); #if !LIGHTMASS_DO_PROCESSING const int32 MaxNumIndirectPhotonPaths = 10; #else const int32 MaxNumIndirectPhotonPaths = 20000; #endif // If the importance volume is valid, only gather enough indirect photon paths to meet IndirectPhotonPathDensity inside the importance volume if (!PhotonMappingSettings.bEmitPhotonsOutsideImportanceVolume && ImportanceBounds.SphereRadius > DELTA) { NumIndirectPhotonPaths = FMath::TruncToInt(Scene.PhotonMappingSettings.IndirectPhotonPathDensity * ImportanceSurfaceAreaMillionUnits); } else if (ImportanceBounds.SphereRadius > DELTA) { NumIndirectPhotonPaths = FMath::TruncToInt(Scene.PhotonMappingSettings.IndirectPhotonPathDensity * ImportanceSurfaceAreaMillionUnits + Scene.PhotonMappingSettings.OutsideImportanceVolumeDensityScale * Scene.PhotonMappingSettings.IndirectPhotonPathDensity * SceneSurfaceAreaMillionUnits); } else { NumIndirectPhotonPaths = FMath::TruncToInt(Scene.PhotonMappingSettings.IndirectPhotonPathDensity * SceneSurfaceAreaMillionUnits); } NumIndirectPhotonPaths = NumIndirectPhotonPaths == appTruncErrorCode ? MaxNumIndirectPhotonPaths : NumIndirectPhotonPaths; NumIndirectPhotonPaths = FMath::Min(NumIndirectPhotonPaths, MaxNumIndirectPhotonPaths); if (NumIndirectPhotonPaths == MaxNumIndirectPhotonPaths) { LogSolverMessage(FString::Printf(TEXT("Clamped the number of indirect photon paths to %u."), MaxNumIndirectPhotonPaths)); } #if !LIGHTMASS_DO_PROCESSING const int32 MaxNumIndirectPhotons = 10; #else const int32 MaxNumIndirectPhotons = 40000000; #endif Stats.NumSecondPassPhotonsRequested = 0; // If the importance volume is valid, only emit enough indirect photons to meet IndirectPhotonDensity inside the importance volume if (!PhotonMappingSettings.bEmitPhotonsOutsideImportanceVolume && ImportanceBounds.SphereRadius > DELTA) { Stats.NumSecondPassPhotonsRequested = Scene.PhotonMappingSettings.IndirectPhotonDensity * ImportanceSurfaceAreaMillionUnits; } else if (ImportanceBounds.SphereRadius > DELTA) { Stats.NumSecondPassPhotonsRequested = Scene.PhotonMappingSettings.IndirectPhotonDensity * ImportanceSurfaceAreaMillionUnits + Scene.PhotonMappingSettings.OutsideImportanceVolumeDensityScale * Scene.PhotonMappingSettings.IndirectPhotonDensity * SceneSurfaceAreaMillionUnits; } else { Stats.NumSecondPassPhotonsRequested = Scene.PhotonMappingSettings.IndirectPhotonDensity * SceneSurfaceAreaMillionUnits; } NumIndirectPhotonsToEmit = FMath::Min(Stats.NumSecondPassPhotonsRequested, (uint64)MaxNumIndirectPhotons); if (NumIndirectPhotonsToEmit == MaxNumIndirectPhotons) { LogSolverMessage(FString::Printf(TEXT("Clamped the number of indirect photons to emit to %.3f million, from %.3f million requested."), MaxNumIndirectPhotons / 1000000.0f, Stats.NumSecondPassPhotonsRequested / 1000000.0f)); } IndirectIrradiancePhotonFraction = FMath::Clamp(Scene.PhotonMappingSettings.IndirectIrradiancePhotonDensity / Scene.PhotonMappingSettings.IndirectPhotonDensity, 0.0f, 1.0f); } /** Emits photons, builds data structures to accelerate photon map lookups, and does any other photon preprocessing required. */ void FStaticLightingSystem::EmitPhotons() { const FBoxSphereBounds3f SceneSphereBounds = FBoxSphereBounds3f(AggregateMesh->GetBounds()); FBoxSphereBounds3f ImportanceVolumeBounds = GetImportanceBounds(); if (ImportanceVolumeBounds.SphereRadius < DELTA) { ImportanceVolumeBounds = SceneSphereBounds; } // Presize for the results from two emitting passes (direct photon emitting, then indirect) mIrradiancePhotons.Empty(NumPhotonWorkRanges * 2); const double StartEmitDirectTime = FPlatformTime::Seconds(); TArray > IndirectPathRays; // Emit photons for the direct photon map, and gather rays which resulted in indirect photon paths. EmitDirectPhotons(ImportanceVolumeBounds, IndirectPathRays, mIrradiancePhotons); const double EndEmitDirectTime = FPlatformTime::Seconds(); Stats.EmitDirectPhotonsTime = EndEmitDirectTime - StartEmitDirectTime; LogSolverMessage(FString::Printf(TEXT("EmitDirectPhotons complete, %.3f million photons emitted in %.1f seconds"), Stats.NumFirstPassPhotonsEmitted / 1000000.0f, Stats.EmitDirectPhotonsTime)); // Let the scene's lights cache information about the indirect path rays, // Which will be used to accelerate light sampling using those paths when emitting indirect photons. for (int32 LightIndex = 0; LightIndex < Lights.Num(); LightIndex++) { FLight* CurrentLight = Lights[LightIndex]; CurrentLight->CachePathRays(IndirectPathRays[LightIndex]); } const double EndCachingIndirectPathsTime = FPlatformTime::Seconds(); Stats.CachingIndirectPhotonPathsTime = EndCachingIndirectPathsTime - EndEmitDirectTime; // Emit photons for the indirect photon map, using the indirect photon paths to guide photon emission. EmitIndirectPhotons(ImportanceVolumeBounds, IndirectPathRays, mIrradiancePhotons); const double EndEmitIndirectTime = FPlatformTime::Seconds(); Stats.EmitIndirectPhotonsTime = EndEmitIndirectTime - EndCachingIndirectPathsTime; LogSolverMessage(FString::Printf(TEXT("EmitIndirectPhotons complete, %.3f million photons emitted in %.1f seconds"), Stats.NumSecondPassPhotonsEmitted / 1000000.0f, Stats.EmitIndirectPhotonsTime)); if (PhotonMappingSettings.bUseIrradiancePhotons) { // Process all irradiance photons and mark ones that have direct photons nearby, // So that we can search for those with a smaller radius when using them for rendering. // This allows more accurate direct shadow transitions with irradiance photons. MarkIrradiancePhotons(ImportanceVolumeBounds, mIrradiancePhotons); const double EndMarkIrradiancePhotonsTime = FPlatformTime::Seconds(); Stats.IrradiancePhotonMarkingTime = EndMarkIrradiancePhotonsTime - EndEmitIndirectTime; LogSolverMessage(FString::Printf(TEXT("Marking Irradiance Photons complete, %.3f million photons marked in %.1f seconds"), Stats.NumIrradiancePhotons / 1000000.0f, Stats.IrradiancePhotonMarkingTime)); if (PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces) { // Cache irradiance photons on surfaces, as an optimization for final gathering. // Final gather rays already know which surface they intersected, so we can do a constant time lookup to find // The nearest irradiance photon instead of doing a photon map gather at the end of each final gather ray. // As an additional benefit, only cached irradiance photons are actually used for rendering, so we only need to calculate irradiance for the used ones. CacheIrradiancePhotons(); Stats.CacheIrradiancePhotonsTime = FPlatformTime::Seconds() - EndMarkIrradiancePhotonsTime; LogSolverMessage(FString::Printf(TEXT("Caching Irradiance Photons complete, %.3f million cache samples in %.1f seconds"), Stats.NumCachedIrradianceSamples / 1000000.0f, Stats.CacheIrradiancePhotonsTime)); } // Calculate irradiance for photons found by the caching on surfaces pass. // This is done as an optimization to final gathering, as described in the paper titled "Faster Photon Map Global Illumination" // The optimization is that irradiance is pre-calculated at a subset of the photons so that final gather rays can just lookup the nearest irradiance photon, // Instead of doing a photon density estimation to calculate irradiance. const double StartCalculateIrradiancePhotonsTime = FPlatformTime::Seconds(); CalculateIrradiancePhotons(ImportanceVolumeBounds, mIrradiancePhotons); Stats.IrradiancePhotonCalculatingTime = FPlatformTime::Seconds() - StartCalculateIrradiancePhotonsTime; LogSolverMessage(FString::Printf(TEXT("Calculate Irradiance Photons complete, %.3f million irradiance calculations in %.1f seconds"), Stats.NumFoundIrradiancePhotons / 1000000.0f, Stats.IrradiancePhotonCalculatingTime)); } // Verify that temporary photon memory has been freed check(DirectPhotonEmittingWorkRanges.Num() == 0 && DirectPhotonEmittingOutputs.Num() == 0 && IndirectPhotonEmittingWorkRanges.Num() == 0 && IndirectPhotonEmittingOutputs.Num() == 0 && IrradianceMarkWorkRanges.Num() == 0 && IrradianceCalculationWorkRanges.Num() == 0 && IrradiancePhotonCachingThreads.Num() == 0); } /** Emits direct photons and generates indirect photon paths. */ void FStaticLightingSystem::EmitDirectPhotons( const FBoxSphereBounds3f& ImportanceBounds, TArray >& IndirectPathRays, TArray>& IrradiancePhotons) { GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing0, 0 ) ); FSceneLightPowerDistribution LightDistribution; // Create a 1d Step Probability Density Function based on the number of direct photons each light wants to gather. LightDistribution.LightPDFs.Empty(Lights.Num()); for (int32 LightIndex = 0; LightIndex < Lights.Num(); LightIndex++) { const FLight* CurrentLight = Lights[LightIndex]; const int32 LightNumDirectPhotons = CurrentLight->GetNumDirectPhotons(PhotonMappingSettings.DirectPhotonDensity); LightDistribution.LightPDFs.Add(LightNumDirectPhotons); } if (Lights.Num() > 0) { // Compute the Cumulative Distribution Function for our step function of light powers CalculateStep1dCDF(LightDistribution.LightPDFs, LightDistribution.LightCDFs, LightDistribution.UnnormalizedIntegral); } IndirectPathRays.Empty(Lights.Num()); IndirectPathRays.AddZeroed(Lights.Num()); // Add irradiance photon array entries for all the work ranges that will be processed const int32 IrradianceArrayStart = IrradiancePhotons.AddZeroed(NumPhotonWorkRanges); const FDirectPhotonEmittingInput Input(ImportanceBounds, LightDistribution); int32 NumIndirectPhotonPathsRemaining = NumIndirectPhotonPaths; // Setup work ranges, which are sections of work that can be done in parallel. DirectPhotonEmittingWorkRanges.Empty(NumPhotonWorkRanges); for (int32 RangeIndex = 0; RangeIndex < NumPhotonWorkRanges - 1; RangeIndex++) { int32 NumIndirectPhotonPathsRange = FMath::Max(NumIndirectPhotonPaths / NumPhotonWorkRanges, 1); if (NumIndirectPhotonPathsRemaining == 0) { NumIndirectPhotonPathsRange = 0; } NumIndirectPhotonPathsRemaining = FMath::Max(NumIndirectPhotonPathsRemaining - NumIndirectPhotonPathsRange, 0); DirectPhotonEmittingWorkRanges.Add(FDirectPhotonEmittingWorkRange(RangeIndex, NumDirectPhotonsToEmit / NumPhotonWorkRanges, NumIndirectPhotonPathsRange)); } // The last work range contains the remainders DirectPhotonEmittingWorkRanges.Add(FDirectPhotonEmittingWorkRange( NumPhotonWorkRanges - 1, NumDirectPhotonsToEmit / NumPhotonWorkRanges + NumDirectPhotonsToEmit % NumPhotonWorkRanges, NumIndirectPhotonPathsRemaining)); DirectPhotonEmittingOutputs.Empty(NumPhotonWorkRanges); for (int32 RangeIndex = 0; RangeIndex < NumPhotonWorkRanges; RangeIndex++) { // Initialize outputs with the appropriate irradiance photon array DirectPhotonEmittingOutputs.Add(FDirectPhotonEmittingOutput(&IrradiancePhotons[IrradianceArrayStart + RangeIndex])); } // Spawn threads to emit direct photons TIndirectArray DirectPhotonEmittingThreads; for (int32 ThreadIndex = 1; ThreadIndex < NumStaticLightingThreads; ThreadIndex++) { FDirectPhotonEmittingThreadRunnable* ThreadRunnable = new FDirectPhotonEmittingThreadRunnable(this, ThreadIndex, Input); DirectPhotonEmittingThreads.Add(ThreadRunnable); const FString ThreadName = FString::Printf(TEXT("DirectPhotonEmittingThread%u"), ThreadIndex); ThreadRunnable->Thread = FRunnableThread::Create(ThreadRunnable, *ThreadName); } const double StartEmittingDirectPhotonsMainThread = FPlatformTime::Seconds(); // Add the photons into a spatial data structure to accelerate their searches later //@todo - should try a kd-tree instead as the distribution of photons is highly non-uniform DirectPhotonMap = FPhotonOctree(ImportanceBounds.Origin, ImportanceBounds.BoxExtent.GetMax()); IrradiancePhotonMap = FIrradiancePhotonOctree(ImportanceBounds.Origin, ImportanceBounds.BoxExtent.GetMax()); Stats.NumDirectPhotonsGathered = 0; Stats.NumDirectIrradiancePhotons = 0; int32 NumIndirectPhotonPathsGathered = 0; int32 NextOutputToProcess = 0; while (DirectPhotonEmittingWorkRangeIndex.GetValue() < DirectPhotonEmittingWorkRanges.Num() || NextOutputToProcess < DirectPhotonEmittingOutputs.Num()) { // Process one work range on the main thread EmitDirectPhotonsThreadLoop(Input, 0); LIGHTINGSTAT(FScopedRDTSCTimer MainThreadProcessTimer(Stats.ProcessDirectPhotonsThreadTime)); // Process the outputs that have been completed by any thread // Outputs are collected from smallest to largest work range index so that the outputs will be deterministic. while (NextOutputToProcess < DirectPhotonEmittingOutputs.Num() && DirectPhotonEmittingOutputs[NextOutputToProcess].OutputComplete > 0) { const FDirectPhotonEmittingOutput& CurrentOutput = DirectPhotonEmittingOutputs[NextOutputToProcess]; for (int32 PhotonIndex = 0; PhotonIndex < CurrentOutput.DirectPhotons.Num(); PhotonIndex++) { // Add direct photons to the direct photon map, which is an octree for now DirectPhotonMap.AddElement(FPhotonElement(CurrentOutput.DirectPhotons[PhotonIndex])); } for (int32 LightIndex = 0; LightIndex < CurrentOutput.IndirectPathRays.Num(); LightIndex++) { // Gather indirect path rays IndirectPathRays[LightIndex].Append(CurrentOutput.IndirectPathRays[LightIndex]); NumIndirectPhotonPathsGathered += CurrentOutput.IndirectPathRays[LightIndex].Num(); } if (PhotonMappingSettings.bUseIrradiancePhotons && PhotonMappingSettings.bUsePhotonDirectLightingInFinalGather) { for (int32 PhotonIndex = 0; PhotonIndex < CurrentOutput.IrradiancePhotons->Num(); PhotonIndex++) { // Add the irradiance photons to an octree IrradiancePhotonMap.AddElement(FIrradiancePhotonElement(PhotonIndex, *CurrentOutput.IrradiancePhotons)); } Stats.NumIrradiancePhotons += CurrentOutput.IrradiancePhotons->Num(); Stats.NumDirectIrradiancePhotons += CurrentOutput.IrradiancePhotons->Num(); } Stats.NumFirstPassPhotonsEmitted += CurrentOutput.NumPhotonsEmitted; NumPhotonsEmittedDirect += CurrentOutput.NumPhotonsEmittedDirect; Stats.NumDirectPhotonsGathered += CurrentOutput.DirectPhotons.Num(); NextOutputToProcess++; Stats.DirectPhotonsTracingThreadTime += CurrentOutput.DirectPhotonsTracingThreadTime; Stats.DirectPhotonsLightSamplingThreadTime += CurrentOutput.DirectPhotonsLightSamplingThreadTime; Stats.DirectCustomAttenuationThreadTime += CurrentOutput.DirectCustomAttenuationThreadTime; } } Stats.EmitDirectPhotonsThreadTime = FPlatformTime::Seconds() - StartEmittingDirectPhotonsMainThread; // Wait until all worker threads have completed for (int32 ThreadIndex = 0; ThreadIndex < DirectPhotonEmittingThreads.Num(); ThreadIndex++) { DirectPhotonEmittingThreads[ThreadIndex].Thread->WaitForCompletion(); DirectPhotonEmittingThreads[ThreadIndex].CheckHealth(); delete DirectPhotonEmittingThreads[ThreadIndex].Thread; DirectPhotonEmittingThreads[ThreadIndex].Thread = NULL; Stats.EmitDirectPhotonsThreadTime += DirectPhotonEmittingThreads[ThreadIndex].ExecutionTime; } if (NumIndirectPhotonPathsGathered != NumIndirectPhotonPaths && GeneralSettings.NumIndirectLightingBounces > 0) { LogSolverMessage(FString::Printf(TEXT("Couldn't gather the requested number of indirect photon paths! %u gathered"), NumIndirectPhotonPathsGathered)); } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (PhotonMappingSettings.bVisualizePhotonPaths) { if (GeneralSettings.ViewSingleBounceNumber < 0 || PhotonMappingSettings.bVisualizeCachedApproximateDirectLighting && GeneralSettings.ViewSingleBounceNumber == 0 || PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber == 1) { if (PhotonMappingSettings.bUseIrradiancePhotons) { int32 NumDirectIrradiancePhotons = 0; for (int32 ArrayIndex = 0; ArrayIndex < IrradiancePhotons.Num(); ArrayIndex++) { NumDirectIrradiancePhotons += IrradiancePhotons[ArrayIndex].Num(); } StartupDebugOutput.IrradiancePhotons.Empty(NumDirectIrradiancePhotons); for (int32 ArrayIndex = 0; ArrayIndex < IrradiancePhotons.Num(); ArrayIndex++) { for (int32 i = 0; i < IrradiancePhotons[ArrayIndex].Num(); i++) { StartupDebugOutput.IrradiancePhotons.Add(FDebugPhoton(0, IrradiancePhotons[ArrayIndex][i].GetPosition(), IrradiancePhotons[ArrayIndex][i].GetSurfaceNormal(), IrradiancePhotons[ArrayIndex][i].GetSurfaceNormal())); } } } else { StartupDebugOutput.DirectPhotons.Empty(Stats.NumDirectPhotonsGathered); for (int32 OutputIndex = 0; OutputIndex < DirectPhotonEmittingOutputs.Num(); OutputIndex++) { const FDirectPhotonEmittingOutput& CurrentOutput = DirectPhotonEmittingOutputs[OutputIndex]; for (int32 i = 0; i < CurrentOutput.DirectPhotons.Num(); i++) { StartupDebugOutput.DirectPhotons.Add(FDebugPhoton(CurrentOutput.DirectPhotons[i].GetId(), CurrentOutput.DirectPhotons[i].GetPosition(), CurrentOutput.DirectPhotons[i].GetIncidentDirection(), CurrentOutput.DirectPhotons[i].GetSurfaceNormal())); } } } } if (GeneralSettings.ViewSingleBounceNumber != 0) { StartupDebugOutput.IndirectPhotonPaths.Empty(NumIndirectPhotonPathsGathered); for (int32 LightIndex = 0; LightIndex < IndirectPathRays.Num(); LightIndex++) { for (int32 RayIndex = 0; RayIndex < IndirectPathRays[LightIndex].Num(); RayIndex++) { StartupDebugOutput.IndirectPhotonPaths.Add(FDebugStaticLightingRay( IndirectPathRays[LightIndex][RayIndex].Start, IndirectPathRays[LightIndex][RayIndex].Start + IndirectPathRays[LightIndex][RayIndex].UnitDirection * IndirectPathRays[LightIndex][RayIndex].Length, true)); } } } } #endif DirectPhotonEmittingWorkRanges.Empty(); DirectPhotonEmittingOutputs.Empty(); GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing0, 0 ) ); } uint32 FDirectPhotonEmittingThreadRunnable::Run() { GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing0, ThreadIndex ) ); const double StartThreadTime = FPlatformTime::Seconds(); FixThreadGroupAffinity(); #if defined(_MSC_VER) && !defined(XBOX) if(!FPlatformMisc::IsDebuggerPresent()) { __try { System->EmitDirectPhotonsThreadLoop(Input, ThreadIndex); } __except( ReportCrash( GetExceptionInformation() ) ) { ErrorMessage = GErrorHist; bTerminatedByError = true; } } else #endif { System->EmitDirectPhotonsThreadLoop(Input, ThreadIndex); } ExecutionTime = FPlatformTime::Seconds() - StartThreadTime; GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing0, ThreadIndex ) ); return 0; } /** Entrypoint for all threads emitting direct photons. */ void FStaticLightingSystem::EmitDirectPhotonsThreadLoop(const FDirectPhotonEmittingInput& Input, int32 ThreadIndex) { while (true) { // Atomically read and increment the next work range index to process. // In this way work ranges are processed on-demand, which ensures consistent end times between threads. // Processing from smallest to largest work range index since the main thread is processing outputs in the same order. const int32 RangeIndex = DirectPhotonEmittingWorkRangeIndex.Increment() - 1; if (RangeIndex < DirectPhotonEmittingWorkRanges.Num()) { EmitDirectPhotonsWorkRange(Input, DirectPhotonEmittingWorkRanges[RangeIndex], DirectPhotonEmittingOutputs[RangeIndex]); if (ThreadIndex == 0) { // Break out of the loop on the main thread after one work range so that it can process any outputs that are ready break; } } else { // Processing has begun for all work ranges break; } } } /** Emits direct photons for a given work range. */ void FStaticLightingSystem::EmitDirectPhotonsWorkRange( const FDirectPhotonEmittingInput& Input, FDirectPhotonEmittingWorkRange WorkRange, FDirectPhotonEmittingOutput& Output) { // No lights in the scene, so no photons to emit if (Lights.Num() == 0 // No light power in the scene, so no photons to shoot || Input.LightDistribution.UnnormalizedIntegral < DELTA) { // Indicate to the main thread that this output is ready for processing FPlatformAtomics::InterlockedIncrement(&DirectPhotonEmittingOutputs[WorkRange.RangeIndex].OutputComplete); return; } Output.IndirectPathRays.Empty(Lights.Num()); Output.IndirectPathRays.AddZeroed(Lights.Num()); for (int32 LightIndex = 0; LightIndex < Output.IndirectPathRays.Num(); LightIndex++) { Output.IndirectPathRays[LightIndex].Empty(WorkRange.TargetNumIndirectPhotonPaths); } if (PhotonMappingSettings.bUseIrradiancePhotons) { // Attempt to preallocate irradiance photons based on the percentage of photons that go into the irradiance photon map. // The actual number of irradiance photons is based on probability. Output.IrradiancePhotons->Empty(FMath::TruncToInt(DirectIrradiancePhotonFraction * DirectPhotonEfficiency * WorkRange.NumDirectPhotonsToEmit)); } FCoherentRayCache CoherentRayCache; // Initialize the random stream using the work range's index, // So that different numbers will be generated for each work range, // While maintaining determinism regardless of the order that work ranges are processed. FLMRandomStream RandomStream(WorkRange.RangeIndex); // Array of rays from each light which resulted in an indirect path. // These are used in the second emitting pass to guide light sampling for indirect photons. Output.DirectPhotons.Empty(FMath::TruncToInt(WorkRange.NumDirectPhotonsToEmit * DirectPhotonEfficiency)); Output.NumPhotonsEmitted = 0; int32 NumIndirectPathRaysGathered = 0; // Emit photons until we reach the limit for this work range, while (Output.NumPhotonsEmitted < WorkRange.NumDirectPhotonsToEmit // Or we haven't found enough indirect photon paths yet. || NumIndirectPathRaysGathered < WorkRange.TargetNumIndirectPhotonPaths) { Output.NumPhotonsEmitted++; // Once we have emitted enough direct photons, // Stop emitting photons if we are getting below 0.2% efficiency for indirect photon paths // This can happen if the scene is close to convex if (Output.NumPhotonsEmitted >= WorkRange.NumDirectPhotonsToEmit && NumIndirectPathRaysGathered < WorkRange.TargetNumIndirectPhotonPaths && Output.NumPhotonsEmitted > WorkRange.TargetNumIndirectPhotonPaths * 500.0f) { break; } int32 NumberOfPathVertices = 0; float LightPDF; float LightIndex; // Pick a light with probability proportional to the light's fraction of the direct photons being gathered for the whole scene Sample1dCDF(Input.LightDistribution.LightPDFs, Input.LightDistribution.LightCDFs, Input.LightDistribution.UnnormalizedIntegral, RandomStream, LightPDF, LightIndex); const int32 QuantizedLightIndex = FMath::TruncToInt(LightIndex * Input.LightDistribution.LightPDFs.Num()); check(QuantizedLightIndex >= 0 && QuantizedLightIndex < Lights.Num()); const FLight* Light = Lights[QuantizedLightIndex]; FLightRay SampleRay; FVector4f LightSourceNormal; FVector2f LightSurfacePosition; float RayDirectionPDF; FLinearColor PathAlpha; { LIGHTINGSTAT(FScopedRDTSCTimer LightSampleTimer(Output.DirectPhotonsLightSamplingThreadTime)); // Generate the first ray for a new path from the light's distribution of emitted light Light->SampleDirection(RandomStream, SampleRay, LightSourceNormal, LightSurfacePosition, RayDirectionPDF, PathAlpha); } // Update the path's throughput based on the probability of picking this light and this direction PathAlpha = PathAlpha / (LightPDF * RayDirectionPDF); if (PathAlpha.R <= 0.0f && PathAlpha.G <= 0.0f && PathAlpha.B <= 0.0f) { // Skip to next photon since the light doesn't emit any energy in this direction continue; } const float BeforeDirectTraceTime = CoherentRayCache.FirstHitRayTraceTime; // Find the first vertex of the photon path FLightRayIntersection PathIntersection; SampleRay.TraceFlags |= LIGHTRAY_FLIP_SIDEDNESS; AggregateMesh->IntersectLightRay(SampleRay, true, true, true, CoherentRayCache, PathIntersection); Output.DirectPhotonsTracingThreadTime += CoherentRayCache.FirstHitRayTraceTime - BeforeDirectTraceTime; const FVector4f WorldPathDirection = SampleRay.Direction.GetUnsafeNormal3(); // Register this photon path as long as it hit a frontface of something in the scene if (PathIntersection.bIntersects && Dot3(WorldPathDirection, PathIntersection.IntersectionVertex.WorldTangentZ) < 0.0f) { { LIGHTINGSTAT(FScopedRDTSCTimer CustomAttenuationTimer(Output.DirectCustomAttenuationThreadTime)); // Allow the light to attenuate in a non-physically correct way PathAlpha *= Light->CustomAttenuation(PathIntersection.IntersectionVertex.WorldPosition, RandomStream, true); } // Apply transmission PathAlpha *= PathIntersection.Transmission; if ((PathAlpha.R < DELTA && PathAlpha.G < DELTA && PathAlpha.B < DELTA) // Ray can hit translucent meshes if they have bCastShadowAsMasked, but we don't have diffuse for translucency, so just terminate || PathIntersection.Mesh->IsTranslucent(PathIntersection.ElementIndex)) { // Skip to the next photon since the path contribution was completely filtered out by transmission or attenuation continue; } NumberOfPathVertices++; // Note: SampleRay.Start is offset from the actual start position, but not enough to matter for the algorithms which make use of the photon's traveled distance. const float RayLength = (SampleRay.Start - PathIntersection.IntersectionVertex.WorldPosition).Size3(); // Create a photon from this path vertex's information const FPhoton NewPhoton(Output.NumPhotonsEmitted, PathIntersection.IntersectionVertex.WorldPosition, RayLength, -WorldPathDirection, PathIntersection.IntersectionVertex.WorldTangentZ, PathAlpha); checkSlow(FLinearColorUtils::AreFloatsValid(PathAlpha)); if (Output.NumPhotonsEmitted < WorkRange.NumDirectPhotonsToEmit // Only deposit photons inside the importance bounds && Input.ImportanceBounds.GetBox().IsInside(PathIntersection.IntersectionVertex.WorldPosition)) { Output.DirectPhotons.Add(NewPhoton); Output.NumPhotonsEmittedDirect = Output.NumPhotonsEmitted; if (PhotonMappingSettings.bUseIrradiancePhotons // Create an irradiance photon for a fraction of the direct photons && RandomStream.GetFraction() < DirectIrradiancePhotonFraction) { const FIrradiancePhoton NewIrradiancePhoton(PathIntersection.IntersectionVertex.WorldPosition, PathIntersection.IntersectionVertex.WorldTangentZ, true); Output.IrradiancePhotons->Add(NewIrradiancePhoton); } } // Continue tracing this path if we don't have enough indirect photon paths yet if (NumIndirectPathRaysGathered < WorkRange.TargetNumIndirectPhotonPaths) { FStaticLightingVertex IntersectionVertexWithTangents(PathIntersection.IntersectionVertex); FVector4f NewWorldPathDirection; float BRDFDirectionPDF; // Generate a new path direction from the BRDF const FLinearColor BRDF = PathIntersection.Mesh->SampleBRDF( IntersectionVertexWithTangents, PathIntersection.ElementIndex, -WorldPathDirection, NewWorldPathDirection, BRDFDirectionPDF, RandomStream); // Terminate if the path has lost all of its energy due to the surface's BRDF if (BRDF.Equals(FLinearColor::Black)) { continue; } const float CosFactor = -Dot3(WorldPathDirection, IntersectionVertexWithTangents.WorldTangentZ); checkSlow(CosFactor >= 0.0f && CosFactor <= 1.0f); const FVector4f RayStart = IntersectionVertexWithTangents.WorldPosition + NewWorldPathDirection * SceneConstants.VisibilityRayOffsetDistance + IntersectionVertexWithTangents.WorldTangentZ * SceneConstants.VisibilityNormalOffsetDistance; const FVector4f RayEnd = IntersectionVertexWithTangents.WorldPosition + NewWorldPathDirection * MaxRayDistance; FLightRay IndirectSampleRay( RayStart, RayEnd, NULL, NULL ); const float BeforeIndirectTraceTime = CoherentRayCache.FirstHitRayTraceTime; FLightRayIntersection NewPathIntersection; IndirectSampleRay.TraceFlags |= LIGHTRAY_FLIP_SIDEDNESS; AggregateMesh->IntersectLightRay(IndirectSampleRay, true, false, false, CoherentRayCache, NewPathIntersection); Output.DirectPhotonsTracingThreadTime += CoherentRayCache.FirstHitRayTraceTime - BeforeIndirectTraceTime; if (NewPathIntersection.bIntersects && Dot3(NewWorldPathDirection, NewPathIntersection.IntersectionVertex.WorldTangentZ) < 0.0f) { // Store the original photon path which led to an indirect photon path, // This will be used in a second pass to guide indirect photon emission. Output.IndirectPathRays[QuantizedLightIndex].Add(FIndirectPathRay(SampleRay.Start, WorldPathDirection, LightSourceNormal, LightSurfacePosition, RayLength)); NumIndirectPathRaysGathered++; } } } } // Indicate to the main thread that this output is ready for processing FPlatformAtomics::InterlockedIncrement(&DirectPhotonEmittingOutputs[WorkRange.RangeIndex].OutputComplete); } void FStaticLightingSystem::BuildPhotonSegmentMap(const FPhotonOctree& SourcePhotonMap, FPhotonSegmentOctree& OutPhotonSegementMap, float AddToSegmentMapChance) { FLMRandomStream RandomStream(12345); for(FPhotonOctree::TConstIterator<> NodeIt(SourcePhotonMap); NodeIt.HasPendingNodes(); NodeIt.Advance()) { const FPhotonOctree::FNode& CurrentNode = NodeIt.GetCurrentNode(); FOREACH_OCTREE_CHILD_NODE(ChildRef) { if (CurrentNode.HasChild(ChildRef)) { NodeIt.PushChild(ChildRef); } } for (FPhotonOctree::ElementConstIt ElementIt(CurrentNode.GetConstElementIt()); ElementIt; ++ElementIt) { const FPhotonElement& PhotonElement = *ElementIt; if (AddToSegmentMapChance >= 1 || RandomStream.GetFraction() < AddToSegmentMapChance) { const int32 NumSegments = FMath::DivideAndRoundUp(PhotonElement.Photon.GetDistance(), PhotonMappingSettings.PhotonSegmentMaxLength); const float InvNumSegments = 1.0f / NumSegments; for (int32 SegmentIndex = 0; SegmentIndex < NumSegments; SegmentIndex++) { FPhotonSegmentElement NewElement(&PhotonElement.Photon, SegmentIndex * InvNumSegments, InvNumSegments); OutPhotonSegementMap.AddElement(NewElement); } } } } } /** Gathers indirect photons based on the indirect photon paths. */ void FStaticLightingSystem::EmitIndirectPhotons( const FBoxSphereBounds3f& ImportanceBounds, const TArray >& IndirectPathRays, TArray>& IrradiancePhotons) { GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing1, 0 ) ); FSceneLightPowerDistribution LightDistribution; // Create a 1d Step Probability Density Function based on light powers, // So that lights are chosen with a probability proportional to their fraction of the total light power in the scene. LightDistribution.LightPDFs.Empty(Lights.Num()); for (int32 LightIndex = 0; LightIndex < Lights.Num(); LightIndex++) { const FLight* CurrentLight = Lights[LightIndex]; LightDistribution.LightPDFs.Add(CurrentLight->Power()); } if (Lights.Num() > 0) { // Compute the Cumulative Distribution Function for our step function of light powers CalculateStep1dCDF(LightDistribution.LightPDFs, LightDistribution.LightCDFs, LightDistribution.UnnormalizedIntegral); } // Add irradiance photon array entries for all the work ranges that will be processed const int32 IndirectIrradianceArrayStart = IrradiancePhotons.AddZeroed(NumPhotonWorkRanges); const FIndirectPhotonEmittingInput Input(ImportanceBounds, LightDistribution, IndirectPathRays); // Setup work ranges, which are sections of work that can be done in parallel. IndirectPhotonEmittingWorkRanges.Empty(NumPhotonWorkRanges); for (int32 RangeIndex = 0; RangeIndex < NumPhotonWorkRanges - 1; RangeIndex++) { IndirectPhotonEmittingWorkRanges.Add(FIndirectPhotonEmittingWorkRange(RangeIndex, NumIndirectPhotonsToEmit / NumPhotonWorkRanges)); } // The last work range will contain the remainder of photons IndirectPhotonEmittingWorkRanges.Add(FIndirectPhotonEmittingWorkRange( NumPhotonWorkRanges - 1, NumIndirectPhotonsToEmit / NumPhotonWorkRanges + NumIndirectPhotonsToEmit % NumPhotonWorkRanges)); IndirectPhotonEmittingOutputs.Empty(NumPhotonWorkRanges); for (int32 RangeIndex = 0; RangeIndex < NumPhotonWorkRanges; RangeIndex++) { // Initialize outputs with the appropriate irradiance photon array IndirectPhotonEmittingOutputs.Add(FIndirectPhotonEmittingOutput(&IrradiancePhotons[IndirectIrradianceArrayStart + RangeIndex])); } // Spawn threads to emit indirect photons TIndirectArray IndirectPhotonEmittingThreads; for (int32 ThreadIndex = 1; ThreadIndex < NumStaticLightingThreads; ThreadIndex++) { FIndirectPhotonEmittingThreadRunnable* ThreadRunnable = new FIndirectPhotonEmittingThreadRunnable(this, ThreadIndex, Input); IndirectPhotonEmittingThreads.Add(ThreadRunnable); const FString ThreadName = FString::Printf(TEXT("IndirectPhotonEmittingThread%u"), ThreadIndex); ThreadRunnable->Thread = FRunnableThread::Create(ThreadRunnable, *ThreadName); } const double StartEmittingIndirectPhotonsMainThread = FPlatformTime::Seconds(); // Add the photons into spatial data structures to accelerate their searches later FirstBouncePhotonMap = FPhotonOctree(ImportanceBounds.Origin, ImportanceBounds.BoxExtent.GetMax()); FirstBounceEscapedPhotonMap = FPhotonOctree(ImportanceBounds.Origin, ImportanceBounds.BoxExtent.GetMax()); SecondBouncePhotonMap = FPhotonOctree(ImportanceBounds.Origin, ImportanceBounds.BoxExtent.GetMax()); Stats.NumIndirectPhotonsGathered = 0; int32 NextOutputToProcess = 0; while (IndirectPhotonEmittingWorkRangeIndex.GetValue() < IndirectPhotonEmittingWorkRanges.Num() || NextOutputToProcess < IndirectPhotonEmittingOutputs.Num()) { // Process one work range on the main thread EmitIndirectPhotonsThreadLoop(Input, 0); LIGHTINGSTAT(FScopedRDTSCTimer MainThreadProcessTimer(Stats.ProcessIndirectPhotonsThreadTime)); // Process the thread's outputs // Outputs are collected from smallest to largest work range index so that the outputs will be deterministic. while (NextOutputToProcess < IndirectPhotonEmittingOutputs.Num() && IndirectPhotonEmittingOutputs[NextOutputToProcess].OutputComplete > 0) { FIndirectPhotonEmittingOutput& CurrentOutput = IndirectPhotonEmittingOutputs[NextOutputToProcess]; for (int32 PhotonIndex = 0; PhotonIndex < CurrentOutput.FirstBouncePhotons.Num(); PhotonIndex++) { FirstBouncePhotonMap.AddElement(FPhotonElement(CurrentOutput.FirstBouncePhotons[PhotonIndex])); } if (PhotonMappingSettings.bUsePhotonSegmentsForVolumeLighting) { for (int32 PhotonIndex = 0; PhotonIndex < CurrentOutput.FirstBounceEscapedPhotons.Num(); PhotonIndex++) { FirstBounceEscapedPhotonMap.AddElement(FPhotonElement(CurrentOutput.FirstBounceEscapedPhotons[PhotonIndex])); } } for (int32 PhotonIndex = 0; PhotonIndex < CurrentOutput.SecondBouncePhotons.Num(); PhotonIndex++) { SecondBouncePhotonMap.AddElement(FPhotonElement(CurrentOutput.SecondBouncePhotons[PhotonIndex])); } if (PhotonMappingSettings.bUseIrradiancePhotons) { for (int32 PhotonIndex = 0; PhotonIndex < CurrentOutput.IrradiancePhotons->Num(); PhotonIndex++) { // Add the irradiance photons to an octree IrradiancePhotonMap.AddElement(FIrradiancePhotonElement(PhotonIndex, *CurrentOutput.IrradiancePhotons)); } Stats.NumIrradiancePhotons += CurrentOutput.IrradiancePhotons->Num(); } Stats.NumSecondPassPhotonsEmitted += CurrentOutput.NumPhotonsEmitted; Stats.LightSamplingThreadTime += CurrentOutput.LightSamplingThreadTime; Stats.IndirectCustomAttenuationThreadTime += CurrentOutput.IndirectCustomAttenuationThreadTime; Stats.IntersectLightRayThreadTime += CurrentOutput.IntersectLightRayThreadTime; Stats.PhotonBounceTracingThreadTime += CurrentOutput.PhotonBounceTracingThreadTime; NumPhotonsEmittedFirstBounce += CurrentOutput.NumPhotonsEmittedFirstBounce; NumPhotonsEmittedSecondBounce += CurrentOutput.NumPhotonsEmittedSecondBounce; Stats.NumIndirectPhotonsGathered += CurrentOutput.FirstBouncePhotons.Num() + CurrentOutput.SecondBouncePhotons.Num(); NextOutputToProcess++; CurrentOutput.FirstBouncePhotons.Empty(); CurrentOutput.SecondBouncePhotons.Empty(); } } FirstBouncePhotonSegmentMap = FPhotonSegmentOctree(ImportanceBounds.Origin, ImportanceBounds.BoxExtent.GetMax()); if (PhotonMappingSettings.bUsePhotonSegmentsForVolumeLighting) { const double SegmentStartTime = FPlatformTime::Seconds(); BuildPhotonSegmentMap(FirstBouncePhotonMap, FirstBouncePhotonSegmentMap, PhotonMappingSettings.GeneratePhotonSegmentChance); BuildPhotonSegmentMap(FirstBounceEscapedPhotonMap, FirstBouncePhotonSegmentMap, 1.0f); const float BuildSegmentMapTime = (float)(FPlatformTime::Seconds() - SegmentStartTime); LogSolverMessage(FString::Printf(TEXT("Built photon segment map in %.1f seconds"), BuildSegmentMapTime)); } Stats.EmitIndirectPhotonsThreadTime = FPlatformTime::Seconds() - StartEmittingIndirectPhotonsMainThread; // Wait until all worker threads have completed for (int32 ThreadIndex = 0; ThreadIndex < IndirectPhotonEmittingThreads.Num(); ThreadIndex++) { IndirectPhotonEmittingThreads[ThreadIndex].Thread->WaitForCompletion(); IndirectPhotonEmittingThreads[ThreadIndex].CheckHealth(); delete IndirectPhotonEmittingThreads[ThreadIndex].Thread; IndirectPhotonEmittingThreads[ThreadIndex].Thread = NULL; Stats.EmitIndirectPhotonsThreadTime += IndirectPhotonEmittingThreads[ThreadIndex].ExecutionTime; } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (PhotonMappingSettings.bVisualizePhotonPaths && PhotonMappingSettings.bUseIrradiancePhotons && GeneralSettings.ViewSingleBounceNumber != 0) { int32 NumIndirectIrradiancePhotons = 0; for (int32 RangeIndex = NumPhotonWorkRanges; RangeIndex < IrradiancePhotons.Num(); RangeIndex++) { NumIndirectIrradiancePhotons += IrradiancePhotons[RangeIndex].Num(); } StartupDebugOutput.IrradiancePhotons.Empty(NumIndirectIrradiancePhotons); for (int32 RangeIndex = NumPhotonWorkRanges; RangeIndex < IrradiancePhotons.Num(); RangeIndex++) { for (int32 i = 0; i < IrradiancePhotons[RangeIndex].Num(); i++) { StartupDebugOutput.IrradiancePhotons.Add(FDebugPhoton(0, IrradiancePhotons[RangeIndex][i].GetPosition(), IrradiancePhotons[RangeIndex][i].GetSurfaceNormal(), IrradiancePhotons[RangeIndex][i].GetSurfaceNormal())); } } } #endif IndirectPhotonEmittingWorkRanges.Empty(); IndirectPhotonEmittingOutputs.Empty(); GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing1, 0 ) ); } uint32 FIndirectPhotonEmittingThreadRunnable::Run() { GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing1, ThreadIndex ) ); const double StartThreadTime = FPlatformTime::Seconds(); FixThreadGroupAffinity(); #if defined(_MSC_VER) && !defined(XBOX) if(!FPlatformMisc::IsDebuggerPresent()) { __try { System->EmitIndirectPhotonsThreadLoop(Input, ThreadIndex); } __except( ReportCrash( GetExceptionInformation() ) ) { ErrorMessage = GErrorHist; bTerminatedByError = true; } } else #endif { System->EmitIndirectPhotonsThreadLoop(Input, ThreadIndex); } const double EndThreadTime = FPlatformTime::Seconds(); EndTime = EndThreadTime - GStartupTime; ExecutionTime = EndThreadTime - StartThreadTime; GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing1, ThreadIndex ) ); return 0; } /** Entrypoint for all threads emitting indirect photons. */ void FStaticLightingSystem::EmitIndirectPhotonsThreadLoop(const FIndirectPhotonEmittingInput& Input, int32 ThreadIndex) { while (true) { // Atomically read and increment the next work range index to process. // In this way work ranges are processed on-demand, which ensures consistent end times between threads. // Processing from smallest to largest work range index since the main thread is processing outputs in the same order. const int32 RangeIndex = IndirectPhotonEmittingWorkRangeIndex.Increment() - 1; if (RangeIndex < IndirectPhotonEmittingWorkRanges.Num()) { EmitIndirectPhotonsWorkRange(Input, IndirectPhotonEmittingWorkRanges[RangeIndex], IndirectPhotonEmittingOutputs[RangeIndex]); if (ThreadIndex == 0) { // Break out of the loop on the main thread after one work range so that it can process any outputs that are ready break; } } else { // Processing has begun for all work ranges break; } } } /** Emits indirect photons for a given work range. */ void FStaticLightingSystem::EmitIndirectPhotonsWorkRange( const FIndirectPhotonEmittingInput& Input, FIndirectPhotonEmittingWorkRange WorkRange, FIndirectPhotonEmittingOutput& Output) { if (Input.IndirectPathRays.Num() == 0 || Input.LightDistribution.UnnormalizedIntegral < DELTA) { // No lights in the scene, so no photons to emit // Indicate to the main thread that this output is ready for processing FPlatformAtomics::InterlockedIncrement(&IndirectPhotonEmittingOutputs[WorkRange.RangeIndex].OutputComplete); return; } //@todo - re-evaluate these sizes Output.FirstBouncePhotons.Empty(FMath::TruncToInt(WorkRange.NumIndirectPhotonsToEmit * .6f * IndirectPhotonEfficiency)); Output.FirstBounceEscapedPhotons.Empty(FMath::TruncToInt(WorkRange.NumIndirectPhotonsToEmit * .6f * IndirectPhotonEfficiency * PhotonMappingSettings.GeneratePhotonSegmentChance)); Output.SecondBouncePhotons.Empty(FMath::TruncToInt(WorkRange.NumIndirectPhotonsToEmit * .4f * IndirectPhotonEfficiency)); if (PhotonMappingSettings.bUseIrradiancePhotons) { // Attempt to preallocate irradiance photons based on the percentage of photons that go into the irradiance photon map. // The actual number of irradiance photons is based on probability. Output.IrradiancePhotons->Empty(FMath::TruncToInt(IndirectIrradiancePhotonFraction * IndirectPhotonEfficiency * WorkRange.NumIndirectPhotonsToEmit)); } FCoherentRayCache CoherentRayCache; // Seed the random number generator at the beginning of each work range, so we get deterministic results regardless of the number of threads being used. FLMRandomStream RandomStream(WorkRange.RangeIndex); const bool bIndirectPhotonsNeeded = WorkRange.NumIndirectPhotonsToEmit > 0 && GeneralSettings.NumIndirectLightingBounces > 0; Output.NumPhotonsEmitted = 0; // Emit photons until we reach the limit for this work range, while (bIndirectPhotonsNeeded && Output.NumPhotonsEmitted < WorkRange.NumIndirectPhotonsToEmit) { Output.NumPhotonsEmitted++; int32 NumberOfPathVertices = 0; FLightRay SampleRay; FLinearColor PathAlpha; const FLight* Light = NULL; { LIGHTINGSTAT(FScopedRDTSCTimer SampleLightTimer(Output.LightSamplingThreadTime)); float LightPDF; float LightIndex; // Pick a light with probability proportional to the light's fraction of the scene's light power Sample1dCDF(Input.LightDistribution.LightPDFs, Input.LightDistribution.LightCDFs, Input.LightDistribution.UnnormalizedIntegral, RandomStream, LightPDF, LightIndex); const int32 QuantizedLightIndex = FMath::TruncToInt(LightIndex * Input.LightDistribution.LightPDFs.Num()); check(QuantizedLightIndex >= 0 && QuantizedLightIndex < Lights.Num()); Light = Lights[QuantizedLightIndex]; float RayDirectionPDF; if (Input.IndirectPathRays[QuantizedLightIndex].Num() > 0) { // Use the indirect path rays to sample the light Light->SampleDirection( Input.IndirectPathRays[QuantizedLightIndex], RandomStream, SampleRay, RayDirectionPDF, PathAlpha); } else { FVector4f LightSourceNormal; FVector2f LightSurfacePosition; // No indirect path rays from this light, sample it uniformly Light->SampleDirection(RandomStream, SampleRay, LightSourceNormal, LightSurfacePosition, RayDirectionPDF, PathAlpha); } // Update the path's throughput based on the probability of picking this light and this direction PathAlpha = PathAlpha / (LightPDF * RayDirectionPDF); checkSlow(FLinearColorUtils::AreFloatsValid(PathAlpha)); if (PathAlpha.R < DELTA && PathAlpha.G < DELTA && PathAlpha.B < DELTA) { // Skip to the next photon since the light doesn't emit any energy in this direction continue; } // Clip the end of the photon path to the importance volume, or skip if the photon path does not intersect the importance volume at all. FVector4f ClippedStart, ClippedEnd; if (!ClipLineWithBox(Input.ImportanceBounds.GetBox(), SampleRay.Start, SampleRay.End, ClippedStart, ClippedEnd)) { continue; } SampleRay.End = ClippedEnd; } // Find the first vertex of the photon path FLightRayIntersection PathIntersection; const float BeforeLightRayTime = CoherentRayCache.FirstHitRayTraceTime; SampleRay.TraceFlags |= LIGHTRAY_FLIP_SIDEDNESS; AggregateMesh->IntersectLightRay(SampleRay, true, true, true, CoherentRayCache, PathIntersection); Output.IntersectLightRayThreadTime += CoherentRayCache.FirstHitRayTraceTime - BeforeLightRayTime; LIGHTINGSTAT(FScopedRDTSCTimer PhotonTracingTimer(Output.PhotonBounceTracingThreadTime)); FVector4f WorldPathDirection = SampleRay.Direction.GetUnsafeNormal3(); // Continue tracing this photon path as long as it hit a frontface of something in the scene while (PathIntersection.bIntersects && Dot3(WorldPathDirection, PathIntersection.IntersectionVertex.WorldTangentZ) < 0.0f) { if (NumberOfPathVertices == 0) { LIGHTINGSTAT(FScopedRDTSCTimer CustomAttenuationTimer(Output.IndirectCustomAttenuationThreadTime)); // Allow the light to attenuate in a non-physically correct way PathAlpha *= Light->CustomAttenuation(PathIntersection.IntersectionVertex.WorldPosition, RandomStream, false); } // Apply transmission PathAlpha *= PathIntersection.Transmission; if (PathAlpha.R < DELTA && PathAlpha.G < DELTA && PathAlpha.B < DELTA) { // Skip to the next photon since the light was completely filtered out by transmission or attenuation break; } checkSlow(FLinearColorUtils::AreFloatsValid(PathAlpha)); NumberOfPathVertices++; // Only deposit photons inside the importance bounds if (Input.ImportanceBounds.GetBox().IsInside(PathIntersection.IntersectionVertex.WorldPosition)) { // Only deposit a photon if it is not a direct lighting path, and we still need to gather more indirect photons if (NumberOfPathVertices > 1 && Output.NumPhotonsEmitted < WorkRange.NumIndirectPhotonsToEmit) { // Note: SampleRay.Start is offset from the actual start position, but not enough to matter for the algorithms which make use of the photon's traveled distance. const float RayLength = (SampleRay.Start - PathIntersection.IntersectionVertex.WorldPosition).Size3(); // Create a photon from this path vertex's information const FPhoton NewPhoton(Output.NumPhotonsEmitted, PathIntersection.IntersectionVertex.WorldPosition, RayLength, -WorldPathDirection, PathIntersection.IntersectionVertex.WorldTangentZ, PathAlpha); bool bShouldCreateIrradiancePhoton = false; if (NumberOfPathVertices == 2) { // This is a first bounce photon Output.FirstBouncePhotons.Add(NewPhoton); Output.NumPhotonsEmittedFirstBounce = Output.NumPhotonsEmitted; // Only allow creating an irradiance photon if one or more indirect bounces are required // The final gather is the first bounce when enabled bShouldCreateIrradiancePhoton = (PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 1) || (!PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 0); } else { Output.SecondBouncePhotons.Add(NewPhoton); Output.NumPhotonsEmittedSecondBounce = Output.NumPhotonsEmitted; // Only allow creating an irradiance photon if two or more indirect bounces are required // The final gather is the first bounce when enabled bShouldCreateIrradiancePhoton = (PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 2) || (!PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 1); } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (PhotonMappingSettings.bVisualizePhotonPaths && !PhotonMappingSettings.bUseIrradiancePhotons && (GeneralSettings.ViewSingleBounceNumber < 0 || PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber > 1 || !PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber > 0)) { FScopeLock DebugOutputLock(&DebugOutputSync); if (StartupDebugOutput.IndirectPhotons.Num() == 0) { StartupDebugOutput.IndirectPhotons.Empty(FMath::TruncToInt(NumIndirectPhotonsToEmit * IndirectPhotonEfficiency)); } StartupDebugOutput.IndirectPhotons.Add(FDebugPhoton(NewPhoton.GetId(), NewPhoton.GetPosition(), SampleRay.Start - NewPhoton.GetPosition(), NewPhoton.GetSurfaceNormal())); } #endif // Create an irradiance photon for a fraction of the deposited photons if (PhotonMappingSettings.bUseIrradiancePhotons && bShouldCreateIrradiancePhoton && RandomStream.GetFraction() < IndirectIrradiancePhotonFraction) { const FIrradiancePhoton NewIrradiancePhoton(NewPhoton.GetPosition(), PathIntersection.IntersectionVertex.WorldTangentZ, false); Output.IrradiancePhotons->Add(NewIrradiancePhoton); } } } // Photon bounce number = NumberOfPathVertices - 1 // But with final gathering, a first bounce photon is second bounce lighting const int32 EffectiveBounceNumber = NumberOfPathVertices - 1 + 1; // Stop tracing this photon due to bounce number if (EffectiveBounceNumber >= GeneralSettings.NumIndirectLightingBounces // Ray can hit translucent meshes if they have bCastShadowAsMasked, but we don't have diffuse for translucency, so just terminate || PathIntersection.Mesh->IsTranslucent(PathIntersection.ElementIndex)) { break; } FStaticLightingVertex IntersectionVertexWithTangents(PathIntersection.IntersectionVertex); FVector4f NewWorldPathDirection; float BRDFDirectionPDF; // Generate a new path direction from the BRDF const FLinearColor BRDF = PathIntersection.Mesh->SampleBRDF( IntersectionVertexWithTangents, PathIntersection.ElementIndex, -WorldPathDirection, NewWorldPathDirection, BRDFDirectionPDF, RandomStream); // Terminate if the path has lost all of its energy due to the surface's BRDF if (BRDF.Equals(FLinearColor::Black) // Terminate if indirect photons are completed || Output.NumPhotonsEmitted >= WorkRange.NumIndirectPhotonsToEmit) { break; } const float CosFactor = -Dot3(WorldPathDirection, IntersectionVertexWithTangents.WorldTangentZ); checkSlow(CosFactor >= 0.0f && CosFactor <= 1.0f); if (NumberOfPathVertices == 1) { // On the first bounce, re-weight the photon's throughput instead of using Russian Roulette to maintain equal weights, // Because NumEmitted/NumDeposited efficiency is more important to first bounce photons than having equal weight, // Since they are used for importance sampling the final gather. // Re-weight the throughput based on the probability of surviving. PathAlpha = PathAlpha * BRDF * CosFactor / BRDFDirectionPDF; } else { const FLinearColor NewPathAlpha = PathAlpha * BRDF * CosFactor / BRDFDirectionPDF; // On second and up bounces, terminate the path with probability proportional to the ratio between the new throughput and the old // This results in a smaller number of photons after surface reflections, but they have the same weight as before the reflection, // Which is desirable to reduce noise from the photon maps, at the cost of lower NumEmitted/NumDeposited efficiency. // See the "Extended Photon Map Implementation" paper for details. // Note: to be physically correct this probability should be clamped to [0,1], however this produces photons with extremely large weights, // So instead we maintain a constant photon weight after the surface interaction, // At the cost of introducing bias and leaking energy for bounces where BRDF * CosFactor / BRDFDirectionPDF > 1. const float ContinueProbability = FLinearColorUtils::LinearRGBToXYZ(NewPathAlpha).G / FLinearColorUtils::LinearRGBToXYZ(PathAlpha).G; const float RandomFloat = RandomStream.GetFraction(); if (RandomFloat > ContinueProbability) { // Terminate due to Russian Roulette break; } PathAlpha = NewPathAlpha / ContinueProbability; } checkSlow(FLinearColorUtils::AreFloatsValid(PathAlpha)); const FVector4f RayStart = IntersectionVertexWithTangents.WorldPosition + NewWorldPathDirection * SceneConstants.VisibilityRayOffsetDistance + IntersectionVertexWithTangents.WorldTangentZ * SceneConstants.VisibilityNormalOffsetDistance; FVector4f RayEnd = IntersectionVertexWithTangents.WorldPosition + NewWorldPathDirection * MaxRayDistance; // Clip photon path end points to the importance volume, so we do not bother tracing rays outside the area that photons can be deposited. // If the photon path does not intersect the importance volume at all, it did not originate from inside the volume, so skip to the next photon. FVector4f ClippedStart, ClippedEnd; if (!ClipLineWithBox(Input.ImportanceBounds.GetBox(), RayStart, RayEnd, ClippedStart, ClippedEnd)) { break; } RayEnd = ClippedEnd; SampleRay = FLightRay( RayStart, RayEnd, NULL, NULL ); // Trace a ray to determine the next vertex of the photon's path. SampleRay.TraceFlags |= LIGHTRAY_FLIP_SIDEDNESS; AggregateMesh->IntersectLightRay(SampleRay, true, true, false, CoherentRayCache, PathIntersection); WorldPathDirection = NewWorldPathDirection; } // First bounce escaped photon if (!PathIntersection.bIntersects && NumberOfPathVertices == 1 && PhotonMappingSettings.bUsePhotonSegmentsForVolumeLighting) { if (RandomStream.GetFraction() < PhotonMappingSettings.GeneratePhotonSegmentChance) { // Apply transmission PathAlpha *= PathIntersection.Transmission; checkSlow(FLinearColorUtils::AreFloatsValid(PathAlpha)); FVector4f RayEnd = SampleRay.Start + WorldPathDirection * Input.ImportanceBounds.SphereRadius * 2; FVector4f ClippedStart, ClippedEnd; if (ClipLineWithBox(Input.ImportanceBounds.GetBox(), SampleRay.Start, RayEnd, ClippedStart, ClippedEnd)) { // Create the description for an escaped photon const FPhoton NewPhoton(Output.NumPhotonsEmitted, ClippedEnd, (ClippedEnd - SampleRay.Start).Size3(), -WorldPathDirection, FVector4f(0, 0, 1), PathAlpha); Output.FirstBounceEscapedPhotons.Add(NewPhoton); } } } } // Indicate to the main thread that this output is ready for processing FPlatformAtomics::InterlockedIncrement(&IndirectPhotonEmittingOutputs[WorkRange.RangeIndex].OutputComplete); } /** Iterates through all irradiance photons, searches for nearby direct photons, and marks the irradiance photon has having direct photon influence if necessary. */ void FStaticLightingSystem::MarkIrradiancePhotons(const FBoxSphereBounds3f& ImportanceBounds, TArray>& IrradiancePhotons) { check(PhotonMappingSettings.bUseIrradiancePhotons); GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing2, 0 ) ); // Setup work ranges for processing the irradiance photons IrradianceMarkWorkRanges.Empty(IrradiancePhotons.Num()); for (int32 WorkRange = 0; WorkRange < IrradiancePhotons.Num(); WorkRange++) { IrradianceMarkWorkRanges.Add(FIrradianceMarkingWorkRange(WorkRange, WorkRange)); } TIndirectArray IrradiancePhotonMarkingThreads; IrradiancePhotonMarkingThreads.Empty(NumStaticLightingThreads); for(int32 ThreadIndex = 1; ThreadIndex < NumStaticLightingThreads; ThreadIndex++) { FIrradiancePhotonMarkingThreadRunnable* ThreadRunnable = new FIrradiancePhotonMarkingThreadRunnable(this, ThreadIndex, IrradiancePhotons); IrradiancePhotonMarkingThreads.Add(ThreadRunnable); const FString ThreadName = FString::Printf(TEXT("IrradiancePhotonMarkingThread%u"), ThreadIndex); ThreadRunnable->Thread = FRunnableThread::Create(ThreadRunnable, *ThreadName); } const double MainThreadStartTime = FPlatformTime::Seconds(); MarkIrradiancePhotonsThreadLoop(0, IrradiancePhotons); Stats.IrradiancePhotonMarkingThreadTime = FPlatformTime::Seconds() - MainThreadStartTime; // Stop the static lighting threads. for(int32 ThreadIndex = 0;ThreadIndex < IrradiancePhotonMarkingThreads.Num();ThreadIndex++) { // Wait for the thread to exit. IrradiancePhotonMarkingThreads[ThreadIndex].Thread->WaitForCompletion(); // Check that it didn't terminate with an error. IrradiancePhotonMarkingThreads[ThreadIndex].CheckHealth(); // Destroy the thread. delete IrradiancePhotonMarkingThreads[ThreadIndex].Thread; // Accumulate each thread's execution time and stats Stats.IrradiancePhotonMarkingThreadTime += IrradiancePhotonMarkingThreads[ThreadIndex].ExecutionTime; } IrradianceMarkWorkRanges.Empty(); GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing2, 0 ) ); } uint32 FIrradiancePhotonMarkingThreadRunnable::Run() { GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing2, ThreadIndex ) ); const double StartThreadTime = FPlatformTime::Seconds(); FixThreadGroupAffinity(); #if defined(_MSC_VER) && !defined(XBOX) if(!FPlatformMisc::IsDebuggerPresent()) { __try { System->MarkIrradiancePhotonsThreadLoop(ThreadIndex, IrradiancePhotons); } __except( ReportCrash( GetExceptionInformation() ) ) { ErrorMessage = GErrorHist; bTerminatedByError = true; } } else #endif { System->MarkIrradiancePhotonsThreadLoop(ThreadIndex, IrradiancePhotons); } const double EndThreadTime = FPlatformTime::Seconds(); EndTime = EndThreadTime - GStartupTime; ExecutionTime = EndThreadTime - StartThreadTime; GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing2, ThreadIndex ) ); return 0; } /** Entry point for all threads marking irradiance photons. */ void FStaticLightingSystem::MarkIrradiancePhotonsThreadLoop( int32 ThreadIndex, TArray>& IrradiancePhotons) { while (true) { // Atomically read and increment the next work range index to process. // In this way work ranges are processed on-demand, which ensures consistent end times between threads. const int32 RangeIndex = IrradianceMarkWorkRangeIndex.Increment() - 1; if (RangeIndex < IrradianceMarkWorkRanges.Num()) { MarkIrradiancePhotonsWorkRange(IrradiancePhotons, IrradianceMarkWorkRanges[RangeIndex]); } else { // Processing has begun for all work ranges break; } } } /** Marks irradiance photons specified by a single work range. */ void FStaticLightingSystem::MarkIrradiancePhotonsWorkRange( TArray>& IrradiancePhotons, FIrradianceMarkingWorkRange WorkRange) { // Temporary array that is reused for all photon searches by this thread, to reduce allocations TArray TempFoundPhotons; TArray& CurrentArray = IrradiancePhotons[WorkRange.IrradiancePhotonArrayIndex]; for (int32 PhotonIndex = 0; PhotonIndex < CurrentArray.Num(); PhotonIndex++) { FIrradiancePhoton& CurrentIrradiancePhoton = CurrentArray[PhotonIndex]; // Only add direct contribution if we are final gathering and at least one bounce is required, if ((PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 0) // Or if photon mapping is being used for direct lighting. || PhotonMappingSettings.bVisualizeCachedApproximateDirectLighting) { // Find a nearby direct photon const bool bHasDirectContribution = FindAnyNearbyPhoton(DirectPhotonMap, CurrentIrradiancePhoton.GetPosition(), PhotonMappingSettings.DirectPhotonSearchDistance, false); if (bHasDirectContribution) { // Mark the irradiance photon has having direct contribution, which will be used to reduce the search radius for this irradiance photon, // In order to get more accurate direct shadow transitions using the photon map. CurrentIrradiancePhoton.SetHasDirectContribution(); } } } } /** Calculates irradiance for photons randomly chosen to precalculate irradiance. */ void FStaticLightingSystem::CalculateIrradiancePhotons(const FBoxSphereBounds3f& ImportanceBounds, TArray>& IrradiancePhotons) { check(PhotonMappingSettings.bUseIrradiancePhotons); //@todo - add a preparing stage for the swarm visualizer //GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing2, 0 ) ); if (!PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces) { // Without bCacheIrradiancePhotonsOnSurfaces, treat all irradiance photons as found since we'll have to calculate irradiance for all of them. Stats.NumFoundIrradiancePhotons = Stats.NumIrradiancePhotons; } if (PhotonMappingSettings.bVisualizeIrradiancePhotonCalculation && Scene.DebugMapping) { float ClosestIrradiancePhotonDistSq = FLT_MAX; // Skip direct irradiance photons if viewing indirect bounces const int32 ArrayStart = GeneralSettings.ViewSingleBounceNumber > 0 ? NumPhotonWorkRanges : 0; // Skip indirect irradiance photons if viewing direct only const int32 ArrayEnd = GeneralSettings.ViewSingleBounceNumber == 0 ? NumPhotonWorkRanges : IrradiancePhotons.Num(); for (int32 ArrayIndex = ArrayStart; ArrayIndex < ArrayEnd; ArrayIndex++) { for (int32 PhotonIndex = 0; PhotonIndex < IrradiancePhotons[ArrayIndex].Num(); PhotonIndex++) { const FIrradiancePhoton& CurrentPhoton = IrradiancePhotons[ArrayIndex][PhotonIndex]; const float CurrentDistSquared = (CurrentPhoton.GetPosition() - Scene.DebugInput.Position).SizeSquared3(); if ((!PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces || CurrentPhoton.IsUsed()) && CurrentDistSquared < ClosestIrradiancePhotonDistSq) { // Debug the closest irradiance photon to the selected position. // NOTE: This is not necessarily the photon that will get cached for the selected texel! // It's not easy to figure out which photon will get cached at this point in the lighting process, so we use the closest instead. //@todo - if bCacheIrradiancePhotonsOnSurfaces is enabled, we can figure out exactly which photon will be used by the selected texel or vertex. ClosestIrradiancePhotonDistSq = CurrentDistSquared; DebugIrradiancePhotonCalculationArrayIndex = ArrayIndex; DebugIrradiancePhotonCalculationPhotonIndex = PhotonIndex; } } } } // Setup work ranges for processing the irradiance photons IrradianceCalculationWorkRanges.Empty(IrradiancePhotons.Num()); for (int32 WorkRange = 0; WorkRange < IrradiancePhotons.Num(); WorkRange++) { IrradianceCalculationWorkRanges.Add(FIrradianceCalculatingWorkRange(WorkRange, WorkRange)); } TIndirectArray IrradiancePhotonThreads; IrradiancePhotonThreads.Empty(NumStaticLightingThreads); for(int32 ThreadIndex = 1; ThreadIndex < NumStaticLightingThreads; ThreadIndex++) { FIrradiancePhotonCalculatingThreadRunnable* ThreadRunnable = new FIrradiancePhotonCalculatingThreadRunnable(this, ThreadIndex, IrradiancePhotons); IrradiancePhotonThreads.Add(ThreadRunnable); const FString ThreadName = FString::Printf(TEXT("IrradiancePhotonCalculatingThread%u"), ThreadIndex); ThreadRunnable->Thread = FRunnableThread::Create(ThreadRunnable, *ThreadName); } const double MainThreadStartTime = FPlatformTime::Seconds(); FCalculateIrradiancePhotonStats MainThreadStats; CalculateIrradiancePhotonsThreadLoop(0, IrradiancePhotons, MainThreadStats); Stats.IrradiancePhotonCalculatingThreadTime = FPlatformTime::Seconds() - MainThreadStartTime; Stats.CalculateIrradiancePhotonStats = MainThreadStats; // Stop the static lighting threads. for(int32 ThreadIndex = 0;ThreadIndex < IrradiancePhotonThreads.Num();ThreadIndex++) { // Wait for the thread to exit. IrradiancePhotonThreads[ThreadIndex].Thread->WaitForCompletion(); // Check that it didn't terminate with an error. IrradiancePhotonThreads[ThreadIndex].CheckHealth(); // Destroy the thread. delete IrradiancePhotonThreads[ThreadIndex].Thread; // Accumulate each thread's execution time and stats Stats.IrradiancePhotonCalculatingThreadTime += IrradiancePhotonThreads[ThreadIndex].ExecutionTime; Stats.CalculateIrradiancePhotonStats += IrradiancePhotonThreads[ThreadIndex].Stats; } IrradianceCalculationWorkRanges.Empty(); // Release all of the direct photon map memory since we are not going to need it later DirectPhotonMap.Destroy(); // Release all of the second bounce photon map memory since it will not be used again SecondBouncePhotonMap.Destroy(); } uint32 FIrradiancePhotonCalculatingThreadRunnable::Run() { const double StartThreadTime = FPlatformTime::Seconds(); FixThreadGroupAffinity(); #if defined(_MSC_VER) && !defined(XBOX) if(!FPlatformMisc::IsDebuggerPresent()) { __try { System->CalculateIrradiancePhotonsThreadLoop(ThreadIndex, IrradiancePhotons, Stats); } __except( ReportCrash( GetExceptionInformation() ) ) { ErrorMessage = GErrorHist; bTerminatedByError = true; } } else #endif { System->CalculateIrradiancePhotonsThreadLoop(ThreadIndex, IrradiancePhotons, Stats); } const double EndThreadTime = FPlatformTime::Seconds(); EndTime = EndThreadTime - GStartupTime; ExecutionTime = EndThreadTime - StartThreadTime; return 0; } /** Main loop that all threads access to calculate irradiance photons. */ void FStaticLightingSystem::CalculateIrradiancePhotonsThreadLoop( int32 ThreadIndex, TArray>& IrradiancePhotons, FCalculateIrradiancePhotonStats& OutStats) { while (true) { // Atomically read and increment the next work range index to process. // In this way work ranges are processed on-demand, which ensures consistent end times between threads. // Processing from smallest to largest work range index since the main thread is processing outputs in the same order. const int32 RangeIndex = IrradianceCalcWorkRangeIndex.Increment() - 1; if (RangeIndex < IrradianceCalculationWorkRanges.Num()) { CalculateIrradiancePhotonsWorkRange(IrradiancePhotons, IrradianceCalculationWorkRanges[RangeIndex], OutStats); } else { // Processing has begun for all work ranges break; } } } /** Calculates irradiance for the photons specified by a single work range. */ void FStaticLightingSystem::CalculateIrradiancePhotonsWorkRange( TArray>& IrradiancePhotons, FIrradianceCalculatingWorkRange WorkRange, FCalculateIrradiancePhotonStats& OutStats) { // Temporary array that is reused for all photon searches by this thread, to reduce allocations TArray TempFoundPhotons; TArray& CurrentArray = IrradiancePhotons[WorkRange.IrradiancePhotonArrayIndex]; for (int32 PhotonIndex = 0; PhotonIndex < CurrentArray.Num(); PhotonIndex++) { FIrradiancePhoton& CurrentIrradiancePhoton = CurrentArray[PhotonIndex]; // If we already cached irradiance photons on surfaces, only calculate irradiance for photons which actually got found. if (PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces && !CurrentIrradiancePhoton.IsUsed()) { continue; } const bool bDebugThisPhoton = PhotonMappingSettings.bVisualizeIrradiancePhotonCalculation && DebugIrradiancePhotonCalculationArrayIndex == WorkRange.IrradiancePhotonArrayIndex && DebugIrradiancePhotonCalculationPhotonIndex == PhotonIndex; FLinearColor AccumulatedIrradiance(FLinearColor::Black); // Only add direct contribution if we are final gathering and at least one bounce is required, if (((PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 0) // Or if photon mapping is being used for direct lighting. || PhotonMappingSettings.bVisualizeCachedApproximateDirectLighting) && PhotonMappingSettings.bUsePhotonDirectLightingInFinalGather) { const FLinearColor DirectPhotonIrradiance = CalculatePhotonIrradiance( DirectPhotonMap, NumPhotonsEmittedDirect, PhotonMappingSettings.NumIrradianceCalculationPhotons, PhotonMappingSettings.DirectPhotonSearchDistance, CurrentIrradiancePhoton, bDebugThisPhoton && GeneralSettings.ViewSingleBounceNumber == 0, TempFoundPhotons, OutStats); checkSlow(FLinearColorUtils::AreFloatsValid(DirectPhotonIrradiance)); // Only add direct contribution if it should be viewed given ViewSingleBounceNumber if (GeneralSettings.ViewSingleBounceNumber < 0 || (PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber == 1) || (!PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber == 0) || (PhotonMappingSettings.bVisualizeCachedApproximateDirectLighting && GeneralSettings.ViewSingleBounceNumber == 0)) { AccumulatedIrradiance = DirectPhotonIrradiance; } } // If we are final gathering, first bounce photons are actually the second lighting bounce since the final gather is the first bounce if ((PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 1) || (!PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 0)) { const FLinearColor FirstBouncePhotonIrradiance = CalculatePhotonIrradiance( FirstBouncePhotonMap, NumPhotonsEmittedFirstBounce, PhotonMappingSettings.NumIrradianceCalculationPhotons, PhotonMappingSettings.IndirectPhotonSearchDistance, CurrentIrradiancePhoton, bDebugThisPhoton && GeneralSettings.ViewSingleBounceNumber == 1, TempFoundPhotons, OutStats); checkSlow(FLinearColorUtils::AreFloatsValid(FirstBouncePhotonIrradiance)); // Only add first bounce contribution if it should be viewed given ViewSingleBounceNumber if (GeneralSettings.ViewSingleBounceNumber < 0 || (PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber == 2) || (!PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber == 1)) { AccumulatedIrradiance += FirstBouncePhotonIrradiance; } // If we are final gathering, second bounce photons are actually the third lighting bounce since the final gather is the first bounce if ((PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 2) || (!PhotonMappingSettings.bUseFinalGathering && GeneralSettings.NumIndirectLightingBounces > 1)) { const FLinearColor SecondBouncePhotonIrradiance = CalculatePhotonIrradiance( SecondBouncePhotonMap, NumPhotonsEmittedSecondBounce, PhotonMappingSettings.NumIrradianceCalculationPhotons, PhotonMappingSettings.IndirectPhotonSearchDistance, CurrentIrradiancePhoton, bDebugThisPhoton && GeneralSettings.ViewSingleBounceNumber > 1, TempFoundPhotons, OutStats); checkSlow(FLinearColorUtils::AreFloatsValid(SecondBouncePhotonIrradiance)); // Only add second and up bounce contribution if it should be viewed given ViewSingleBounceNumber if (GeneralSettings.ViewSingleBounceNumber < 0 || (PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber == 3) || (!PhotonMappingSettings.bUseFinalGathering && GeneralSettings.ViewSingleBounceNumber == 2)) { AccumulatedIrradiance += SecondBouncePhotonIrradiance; } } } CurrentIrradiancePhoton.SetIrradiance(AccumulatedIrradiance); } } /** Cache irradiance photons on surfaces. */ void FStaticLightingSystem::CacheIrradiancePhotons() { check(PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces); for(int32 ThreadIndex = 1; ThreadIndex < NumStaticLightingThreads; ThreadIndex++) { FMappingProcessingThreadRunnable* ThreadRunnable = new FMappingProcessingThreadRunnable(this, ThreadIndex, StaticLightingTask_CacheIrradiancePhotons); IrradiancePhotonCachingThreads.Add(ThreadRunnable); const FString ThreadName = FString::Printf(TEXT("IrradiancePhotonCachingThread%u"), ThreadIndex); ThreadRunnable->Thread = FRunnableThread::Create(ThreadRunnable, *ThreadName); } // Start the static lighting thread loop on the main thread, too. // Once it returns, all static lighting mappings have begun processing. CacheIrradiancePhotonsThreadLoop(0, true); // Stop the static lighting threads. for(int32 ThreadIndex = 0;ThreadIndex < IrradiancePhotonCachingThreads.Num();ThreadIndex++) { // Wait for the thread to exit. IrradiancePhotonCachingThreads[ThreadIndex].Thread->WaitForCompletion(); // Check that it didn't terminate with an error. IrradiancePhotonCachingThreads[ThreadIndex].CheckHealth(); // Destroy the thread. delete IrradiancePhotonCachingThreads[ThreadIndex].Thread; } IrradiancePhotonCachingThreads.Empty(); IrradiancePhotonMap.Destroy(); } /** Main loop that all threads access to cache irradiance photons. */ void FStaticLightingSystem::CacheIrradiancePhotonsThreadLoop(int32 ThreadIndex, bool bIsMainThread) { GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing3, ThreadIndex ) ); bool bIsDone = false; while (!bIsDone) { // Atomically read and increment the next mapping index to process. const int32 MappingIndex = NextMappingToCacheIrradiancePhotonsOn.Increment() - 1; if (MappingIndex < AllMappings.Num()) { // If this is the main thread, update progress and apply completed static lighting. if (bIsMainThread) { // Check the health of all static lighting threads. for (int32 ThreadIndexIter = 0; ThreadIndexIter < IrradiancePhotonCachingThreads.Num(); ThreadIndexIter++) { IrradiancePhotonCachingThreads[ThreadIndexIter].CheckHealth(); } } FStaticLightingTextureMapping* TextureMapping = AllMappings[MappingIndex]->GetTextureMapping(); if (TextureMapping) { CacheIrradiancePhotonsTextureMapping(TextureMapping); } } else { // Processing has begun for all mappings. bIsDone = true; } } GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Preparing3, ThreadIndex ) ); } /** Returns true if a photon was found within MaxPhotonSearchDistance. */ bool FStaticLightingSystem::FindAnyNearbyPhoton( const FPhotonOctree& PhotonMap, const FVector4f& SearchPosition, float MaxPhotonSearchDistance, bool bDebugThisLookup) const { FPlatformAtomics::InterlockedIncrement(&Stats.NumPhotonGathers); const FBox3f SearchBox = FBox3f::BuildAABB(SearchPosition, FVector4f(MaxPhotonSearchDistance, MaxPhotonSearchDistance, MaxPhotonSearchDistance)); for (FPhotonOctree::TConstIterator<> OctreeIt(PhotonMap); OctreeIt.HasPendingNodes(); OctreeIt.Advance()) { const FPhotonOctree::FNode& CurrentNode = OctreeIt.GetCurrentNode(); const FOctreeNodeContext& CurrentContext = OctreeIt.GetCurrentContext(); // Push children onto the iterator stack if they intersect the query box if (!CurrentNode.IsLeaf()) { FOREACH_OCTREE_CHILD_NODE(ChildRef) { if (CurrentNode.HasChild(ChildRef)) { const FOctreeNodeContext ChildContext = CurrentContext.GetChildContext(ChildRef); if (ChildContext.Bounds.GetBox().Intersect(SearchBox)) { OctreeIt.PushChild(ChildRef); } } } } // Iterate over all photons in the nodes intersecting the query box for (FPhotonOctree::ElementConstIt MeshIt(CurrentNode.GetConstElementIt()); MeshIt; ++MeshIt) { const FPhotonElement& PhotonElement = *MeshIt; const float DistanceSquared = (PhotonElement.Photon.GetPosition() - SearchPosition).SizeSquared3(); // Only searching for photons closer than the max distance if (DistanceSquared < MaxPhotonSearchDistance * MaxPhotonSearchDistance) { #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugThisLookup && PhotonMappingSettings.bVisualizePhotonGathers && &PhotonMap == &DirectPhotonMap) { StartupDebugOutput.bDirectPhotonValid = true; StartupDebugOutput.GatheredDirectPhoton = FDebugPhoton(PhotonElement.Photon.GetId(), PhotonElement.Photon.GetPosition(), PhotonElement.Photon.GetIncidentDirection(), PhotonElement.Photon.GetSurfaceNormal()); } #endif return true; } } } return false; } /** * Searches the given photon map for the nearest NumPhotonsToFind photons to SearchPosition using an iterative process, * Unless the start and max search distances are the same, in which case all photons in that distance will be returned. * The iterative search starts at StartPhotonSearchDistance and doubles the search distance until enough photons are found or the distance is greater than MaxPhotonSearchDistance. * @return - the furthest found photon's distance squared from SearchPosition, unless the start and max search distances are the same, * in which case FMath::Square(MaxPhotonSearchDistance) will be returned. */ float FStaticLightingSystem::FindNearbyPhotonsIterative( const FPhotonOctree& PhotonMap, const FVector4f& SearchPosition, const FVector4f& SearchNormal, int32 NumPhotonsToFind, float StartPhotonSearchDistance, float MaxPhotonSearchDistance, bool bDebugSearchResults, bool bDebugSearchProcess, TArray& FoundPhotons, FFindNearbyPhotonStats& SearchStats) const { FPlatformAtomics::InterlockedIncrement(&Stats.NumPhotonGathers); SearchStats.NumIterativePhotonMapSearches++; // Only enforce the search number if the start and max distances are not equal const bool bEnforceSearchNumber = !FMath::IsNearlyEqual(StartPhotonSearchDistance, MaxPhotonSearchDistance); float SearchDistance = StartPhotonSearchDistance; float FurthestPhotonDistanceSquared = 0.0f; #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess) { // Verify that only one search is debugged // This will not always catch multiple searches due to re-entrance by multiple threads checkSlow(StartupDebugOutput.GatheredPhotonNodes.Num() == 0); } #endif // Continue searching until we have found enough photons or have exceeded the max search distance while (FoundPhotons.Num() < NumPhotonsToFind && SearchDistance <= MaxPhotonSearchDistance) { #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess) { // Only capture the nodes visited on the last iteration StartupDebugOutput.GatheredPhotonNodes.Empty(); } #endif FurthestPhotonDistanceSquared = SearchDistance * SearchDistance; // Presize to avoid unnecessary allocations // Empty last search iteration's results so we don't have to use AddUniqueItem FoundPhotons.Empty(FMath::Max(NumPhotonsToFind, FoundPhotons.Num() + FoundPhotons.GetSlack())); const FBox3f SearchBox = FBox3f::BuildAABB(SearchPosition, FVector4f(SearchDistance, SearchDistance, SearchDistance)); for (FPhotonOctree::TConstIterator> OctreeIt(PhotonMap); OctreeIt.HasPendingNodes(); OctreeIt.Advance()) { const FPhotonOctree::FNode& CurrentNode = OctreeIt.GetCurrentNode(); const FOctreeNodeContext& CurrentContext = OctreeIt.GetCurrentContext(); { LIGHTINGSTAT(FScopedRDTSCTimer PushingChildrenTimer(SearchStats.PushingOctreeChildrenThreadTime)); // Push children onto the iterator stack if they intersect the query box if (!CurrentNode.IsLeaf()) { FOREACH_OCTREE_CHILD_NODE(ChildRef) { if (CurrentNode.HasChild(ChildRef)) { const FOctreeNodeContext ChildContext = CurrentContext.GetChildContext(ChildRef); if (ChildContext.Bounds.GetBox().Intersect(SearchBox)) { #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess) { StartupDebugOutput.GatheredPhotonNodes.Add(FDebugOctreeNode(ChildContext.Bounds.Center, ChildContext.Bounds.Extent)); } #endif OctreeIt.PushChild(ChildRef); } } } } } LIGHTINGSTAT(FScopedRDTSCTimer ProcessingElementsTimer(SearchStats.ProcessingOctreeElementsThreadTime)); // Iterate over all photons in the nodes intersecting the query box for (FPhotonOctree::ElementConstIt MeshIt(CurrentNode.GetConstElementIt()); MeshIt; ++MeshIt) { const FPhotonElement& PhotonElement = *MeshIt; const float DistanceSquared = (PhotonElement.Photon.GetPosition() - SearchPosition).SizeSquared3(); const float CosNormalTheta = Dot3(SearchNormal, PhotonElement.Photon.GetSurfaceNormal()); const float CosIncidentDirectionTheta = Dot3(SearchNormal, PhotonElement.Photon.GetIncidentDirection()); // Only searching for photons closer than the max distance if (DistanceSquared < FurthestPhotonDistanceSquared // Whose normal is within the specified angle from the search normal && CosNormalTheta > PhotonMappingSettings.PhotonSearchAngleThreshold // And whose incident direction is in the same hemisphere as the search normal. && CosIncidentDirectionTheta > 0.0f) { if (bEnforceSearchNumber) { if (FoundPhotons.Num() < NumPhotonsToFind) { FoundPhotons.Add(PhotonElement.Photon); } else { checkSlow(FoundPhotons.Num() == NumPhotonsToFind); float FurthestFoundPhotonDistSq = 0; int32 FurthestFoundPhotonIndex = -1; // Find the furthest photon // This could be accelerated with a heap instead of doing an O(n) search LIGHTINGSTAT(FScopedRDTSCTimer FindingFurthestTimer(SearchStats.FindingFurthestPhotonThreadTime)); for (int32 PhotonIndex = 0; PhotonIndex < FoundPhotons.Num(); PhotonIndex++) { const float CurrentDistanceSquared = (FoundPhotons[PhotonIndex].GetPosition() - SearchPosition).SizeSquared3(); if (CurrentDistanceSquared > FurthestFoundPhotonDistSq) { FurthestFoundPhotonDistSq = CurrentDistanceSquared; FurthestFoundPhotonIndex = PhotonIndex; } } checkSlow(FurthestFoundPhotonIndex >= 0); FurthestPhotonDistanceSquared = FurthestFoundPhotonDistSq; if (DistanceSquared < FurthestFoundPhotonDistSq) { // Replace the furthest photon with the new photon since the new photon is closer FoundPhotons[FurthestFoundPhotonIndex] = PhotonElement.Photon; } } } else { FoundPhotons.Add(PhotonElement.Photon); } } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess) { StartupDebugOutput.IrradiancePhotons.Add(FDebugPhoton(PhotonElement.Photon.GetId(), PhotonElement.Photon.GetPosition(), PhotonElement.Photon.GetIncidentDirection(), PhotonElement.Photon.GetSurfaceNormal())); } #endif } } // Double the search radius for each iteration SearchDistance *= 2.0f; SearchStats.NumSearchIterations++; } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess || bDebugSearchResults && PhotonMappingSettings.bVisualizePhotonGathers) { // Assuming that only importance photons are debugged and enforce the search number if (bDebugSearchResults && bEnforceSearchNumber) { for (int32 i = 0; i < FoundPhotons.Num(); i++) { StartupDebugOutput.GatheredImportancePhotons.Add(FDebugPhoton(FoundPhotons[i].GetId(), FoundPhotons[i].GetPosition(), FoundPhotons[i].GetIncidentDirection(), FoundPhotons[i].GetSurfaceNormal())); } } else { for (int32 i = 0; i < FoundPhotons.Num(); i++) { StartupDebugOutput.GatheredPhotons.Add(FDebugPhoton(FoundPhotons[i].GetId(), FoundPhotons[i].GetPosition(), FoundPhotons[i].GetIncidentDirection(), FoundPhotons[i].GetSurfaceNormal())); } } } #endif return FurthestPhotonDistanceSquared; } /** * Searches a volume segment map for photons. Can be used at any point in space, not just on surfaces. */ float FStaticLightingSystem::FindNearbyPhotonsInVolumeIterative( const FPhotonSegmentOctree& PhotonSegmentMap, const FVector4f& SearchPosition, int32 NumPhotonsToFind, float StartPhotonSearchDistance, float MaxPhotonSearchDistance, TArray& FoundPhotonSegments, bool bDebugThisLookup) const { FPlatformAtomics::InterlockedIncrement(&Stats.NumPhotonGathers); float SearchDistance = StartPhotonSearchDistance; float FurthestPhotonDistanceSquared = 0.0f; // Continue searching until we have found enough photons or have exceeded the max search distance while (FoundPhotonSegments.Num() < NumPhotonsToFind && SearchDistance <= MaxPhotonSearchDistance) { FurthestPhotonDistanceSquared = SearchDistance * SearchDistance; // Presize to avoid unnecessary allocations // Empty last search iteration's results so we don't have to use AddUniqueItem FoundPhotonSegments.Empty(FMath::Max(NumPhotonsToFind, FoundPhotonSegments.Num() + FoundPhotonSegments.GetSlack())); const FBox3f SearchBox = FBox3f::BuildAABB(SearchPosition, FVector4f(SearchDistance, SearchDistance, SearchDistance)); for (FPhotonSegmentOctree::TConstIterator> OctreeIt(PhotonSegmentMap); OctreeIt.HasPendingNodes(); OctreeIt.Advance()) { const FPhotonSegmentOctree::FNode& CurrentNode = OctreeIt.GetCurrentNode(); const FOctreeNodeContext& CurrentContext = OctreeIt.GetCurrentContext(); { // Push children onto the iterator stack if they intersect the query box if (!CurrentNode.IsLeaf()) { FOREACH_OCTREE_CHILD_NODE(ChildRef) { if (CurrentNode.HasChild(ChildRef)) { const FOctreeNodeContext ChildContext = CurrentContext.GetChildContext(ChildRef); if (ChildContext.Bounds.GetBox().Intersect(SearchBox)) { OctreeIt.PushChild(ChildRef); } } } } } // Iterate over all photons in the nodes intersecting the query box for (FPhotonSegmentOctree::ElementConstIt MeshIt(CurrentNode.GetConstElementIt()); MeshIt; ++MeshIt) { const FPhotonSegmentElement& PhotonSegmentElement = *MeshIt; const float SegmentDistanceSquared = PhotonSegmentElement.ComputeSquaredDistanceToPoint(SearchPosition); // Only searching for photons closer than the max distance if (SegmentDistanceSquared < FurthestPhotonDistanceSquared) { bool bNewPhoton = true; for (int32 i = 0; i < FoundPhotonSegments.Num(); i++) { if (FoundPhotonSegments[i].Photon == PhotonSegmentElement.Photon) { bNewPhoton = false; break; } } if (bNewPhoton) { if (FoundPhotonSegments.Num() < NumPhotonsToFind) { FoundPhotonSegments.Add(PhotonSegmentElement); } else { checkSlow(FoundPhotonSegments.Num() == NumPhotonsToFind); float FurthestFoundPhotonDistSq = 0; int32 FurthestFoundPhotonIndex = -1; // Find the furthest photon for (int32 PhotonIndex = 0; PhotonIndex < FoundPhotonSegments.Num(); PhotonIndex++) { const float OtherSegmentDistanceSquared = FoundPhotonSegments[PhotonIndex].ComputeSquaredDistanceToPoint(SearchPosition); if (OtherSegmentDistanceSquared > FurthestFoundPhotonDistSq) { FurthestFoundPhotonDistSq = OtherSegmentDistanceSquared; FurthestFoundPhotonIndex = PhotonIndex; } } checkSlow(FurthestFoundPhotonIndex >= 0); FurthestPhotonDistanceSquared = FurthestFoundPhotonDistSq; if (SegmentDistanceSquared < FurthestFoundPhotonDistSq) { // Replace the furthest photon with the new photon since the new photon is closer FoundPhotonSegments[FurthestFoundPhotonIndex] = PhotonSegmentElement; } } } } } } // Double the search radius for each iteration SearchDistance *= 2.0f; } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugThisLookup && PhotonMappingSettings.bVisualizePhotonGathers) { for (int32 i = 0; i < FoundPhotonSegments.Num(); i++) { const FPhoton& Photon = *FoundPhotonSegments[i].Photon; StartupDebugOutput.GatheredImportancePhotons.Add(FDebugPhoton(Photon.GetId(), Photon.GetPosition(), Photon.GetIncidentDirection(), Photon.GetSurfaceNormal())); } } #endif return FurthestPhotonDistanceSquared; } struct FOctreeNodeRefAndDistance { FOctreeChildNodeRef NodeRef; float DistanceSquared; FORCEINLINE FOctreeNodeRefAndDistance(FOctreeChildNodeRef InNodeRef, float InDistanceSquared) : NodeRef(InNodeRef), DistanceSquared(InDistanceSquared) {} }; /** * Searches the given photon map for the nearest NumPhotonsToFind photons to SearchPosition by sorting octree nodes nearest to furthest. * @return - the furthest found photon's distance squared from SearchPosition. */ float FStaticLightingSystem::FindNearbyPhotonsSorted( const FPhotonOctree& PhotonMap, const FVector4f& SearchPosition, const FVector4f& SearchNormal, int32 NumPhotonsToFind, float MaxPhotonSearchDistance, bool bDebugSearchResults, bool bDebugSearchProcess, TArray& FoundPhotons, FFindNearbyPhotonStats& SearchStats) const { FPlatformAtomics::InterlockedIncrement(&Stats.NumPhotonGathers); float FurthestPhotonDistanceSquared = MaxPhotonSearchDistance * MaxPhotonSearchDistance; #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess) { // Verify that only one search is debugged // This will not always catch multiple searches due to re-entrance by multiple threads checkSlow(StartupDebugOutput.GatheredPhotonNodes.Num() == 0); } #endif // Presize to avoid unnecessary allocations FoundPhotons.Empty(FMath::Max(NumPhotonsToFind, FoundPhotons.Num() + FoundPhotons.GetSlack())); for (FPhotonOctree::TConstIterator> OctreeIt(PhotonMap); OctreeIt.HasPendingNodes(); OctreeIt.Advance()) { SearchStats.NumOctreeNodesVisited++; const FOctreeNodeContext& CurrentContext = OctreeIt.GetCurrentContext(); const float ClosestNodePointDistanceSquared = (CurrentContext.Bounds.Center - SearchPosition).SizeSquared3() - CurrentContext.Bounds.Extent.SizeSquared3(); if (ClosestNodePointDistanceSquared > FurthestPhotonDistanceSquared && !CurrentContext.Bounds.GetBox().IsInside(SearchPosition)) { // Skip nodes that don't contain the search position and whose closest point is further than FurthestPhotonDistanceSquared // This check was already done before pushing the node, but FurthestPhotonDistanceSquared may have been reduced since then //@todo - can we skip all remaining nodes too? Nodes are pushed from closest to furthest. continue; } const FPhotonOctree::FNode& CurrentNode = OctreeIt.GetCurrentNode(); { LIGHTINGSTAT(FScopedRDTSCTimer ProcessingElementsTimer(SearchStats.ProcessingOctreeElementsThreadTime)); // Iterate over all photons in the nodes intersecting the query box for (FPhotonOctree::ElementConstIt MeshIt(CurrentNode.GetConstElementIt()); MeshIt; ++MeshIt) { SearchStats.NumElementsTested++; const FPhotonElement& PhotonElement = *MeshIt; const float DistanceSquared = (PhotonElement.Photon.GetPosition() - SearchPosition).SizeSquared3(); const float CosNormalTheta = Dot3(SearchNormal, PhotonElement.Photon.GetSurfaceNormal()); const float CosIncidentDirectionTheta = Dot3(SearchNormal, PhotonElement.Photon.GetIncidentDirection()); // Only searching for photons closer than the max distance if (DistanceSquared < FurthestPhotonDistanceSquared // Whose normal is within the specified angle from the search normal && CosNormalTheta > PhotonMappingSettings.PhotonSearchAngleThreshold // And whose incident direction is in the same hemisphere as the search normal. && CosIncidentDirectionTheta > 0.0f) { SearchStats.NumElementsAccepted++; if (FoundPhotons.Num() < NumPhotonsToFind) { FoundPhotons.Add(PhotonElement.Photon); } else { checkSlow(FoundPhotons.Num() == NumPhotonsToFind); float FurthestFoundPhotonDistSq = 0; int32 FurthestFoundPhotonIndex = -1; // Find the furthest photon // This could be accelerated with a heap instead of doing an O(n) search LIGHTINGSTAT(FScopedRDTSCTimer FindingFurthestTimer(SearchStats.FindingFurthestPhotonThreadTime)); for (int32 PhotonIndex = FoundPhotons.Num() - 1; PhotonIndex >= 0; PhotonIndex--) { const float CurrentDistanceSquared = (FoundPhotons[PhotonIndex].GetPosition() - SearchPosition).SizeSquared3(); if (CurrentDistanceSquared > FurthestFoundPhotonDistSq) { FurthestFoundPhotonDistSq = CurrentDistanceSquared; FurthestFoundPhotonIndex = PhotonIndex; } } checkSlow(FurthestFoundPhotonIndex >= 0); FurthestPhotonDistanceSquared = FurthestFoundPhotonDistSq; if (DistanceSquared < FurthestFoundPhotonDistSq) { // Replace the furthest photon with the new photon since the new photon is closer FoundPhotons[FurthestFoundPhotonIndex] = PhotonElement.Photon; } } } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess) { StartupDebugOutput.IrradiancePhotons.Add(FDebugPhoton(PhotonElement.Photon.GetId(), PhotonElement.Photon.GetPosition(), PhotonElement.Photon.GetIncidentDirection(), PhotonElement.Photon.GetSurfaceNormal())); } #endif } } LIGHTINGSTAT(FScopedRDTSCTimer PushingChildrenTimer(SearchStats.PushingOctreeChildrenThreadTime)); // Push children onto the iterator stack if they intersect the query box if (!CurrentNode.IsLeaf()) { TArray > ChildrenInRange; bool bAllNodesZeroDistance = true; FOREACH_OCTREE_CHILD_NODE(ChildRef) { if (CurrentNode.HasChild(ChildRef)) { SearchStats.NumOctreeNodesTested++; const FOctreeNodeContext ChildContext = CurrentContext.GetChildContext(ChildRef); const bool ChildContainsSearchPosition = ChildContext.Bounds.GetBox().IsInside(SearchPosition); const float ClosestChildPointDistanceSquared = ChildContainsSearchPosition ? 0 : FMath::Max((ChildContext.Bounds.Center - SearchPosition).SizeSquared3() - ChildContext.Bounds.Extent.SizeSquared3(), 0.0f); // Only visit nodes that either contain the search position or whose closest point is closer than FurthestPhotonDistanceSquared if (ClosestChildPointDistanceSquared <= FurthestPhotonDistanceSquared) { #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess) { StartupDebugOutput.GatheredPhotonNodes.Add(FDebugOctreeNode(ChildContext.Bounds.Center, ChildContext.Bounds.Extent)); } #endif bAllNodesZeroDistance = bAllNodesZeroDistance && ClosestChildPointDistanceSquared < DELTA; ChildrenInRange.Add(FOctreeNodeRefAndDistance(ChildRef, ClosestChildPointDistanceSquared)); } } } if (!bAllNodesZeroDistance && ChildrenInRange.Num() > 1) { // Used to sort FOctreeNodeRefAndDistances from smallest DistanceSquared to largest. struct FCompareNodeDistance { FORCEINLINE bool operator()(const FOctreeNodeRefAndDistance& A, const FOctreeNodeRefAndDistance& B) const { return A.DistanceSquared < B.DistanceSquared; } }; // Sort the nodes from closest to furthest ChildrenInRange.Sort(FCompareNodeDistance()); } for (int32 NodeIndex = 0; NodeIndex < ChildrenInRange.Num(); NodeIndex++) { OctreeIt.PushChild(ChildrenInRange[NodeIndex].NodeRef); } } } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugSearchProcess || bDebugSearchResults && PhotonMappingSettings.bVisualizePhotonGathers) { // Assuming that only importance photons are debugged if (bDebugSearchResults) { for (int32 i = 0; i < FoundPhotons.Num(); i++) { StartupDebugOutput.GatheredImportancePhotons.Add(FDebugPhoton(FoundPhotons[i].GetId(), FoundPhotons[i].GetPosition(), FoundPhotons[i].GetIncidentDirection(), FoundPhotons[i].GetSurfaceNormal())); } } else { for (int32 i = 0; i < FoundPhotons.Num(); i++) { StartupDebugOutput.GatheredPhotons.Add(FDebugPhoton(FoundPhotons[i].GetId(), FoundPhotons[i].GetPosition(), FoundPhotons[i].GetIncidentDirection(), FoundPhotons[i].GetSurfaceNormal())); } } } #endif return FurthestPhotonDistanceSquared; } /** Comparison class to sort an array of photons into ascending order of distance to ComparePosition. */ class FCompareNearestIrradiancePhotons { public: FCompareNearestIrradiancePhotons(const FVector4f& InPosition) : ComparePosition(InPosition) {} inline bool operator()(const FIrradiancePhoton& A, const FIrradiancePhoton& B) const { const float DistanceSquaredA = (A.GetPosition() - ComparePosition).SizeSquared3(); const float DistanceSquaredB = (B.GetPosition() - ComparePosition).SizeSquared3(); return DistanceSquaredA < DistanceSquaredB; } private: const FVector4f ComparePosition; }; /** Finds the nearest irradiance photon, if one exists. */ FIrradiancePhoton* FStaticLightingSystem::FindNearestIrradiancePhoton( const FMinimalStaticLightingVertex& Vertex, FStaticLightingMappingContext& MappingContext, TArray& TempIrradiancePhotons, bool bVisibleOnly, bool bDebugThisLookup) const { MappingContext.Stats.NumIrradiancePhotonMapSearches++; FIrradiancePhoton* ClosestPhoton = NULL; // Traverse the octree with the maximum distance required const float SearchDistance = FMath::Max(PhotonMappingSettings.DirectPhotonSearchDistance, PhotonMappingSettings.IndirectPhotonSearchDistance); float ClosestDistanceSquared = FMath::Square(SearchDistance); // Empty the temporary array without reallocating TempIrradiancePhotons.Empty(TempIrradiancePhotons.Num() + TempIrradiancePhotons.GetSlack()); const FBox3f SearchBox = FBox3f::BuildAABB(Vertex.WorldPosition, FVector4f(SearchDistance, SearchDistance, SearchDistance)); { LIGHTINGSTAT(FScopedRDTSCTimer OctreeTraversal(MappingContext.Stats.IrradiancePhotonOctreeTraversalTime)); for (FIrradiancePhotonOctree::TConstIterator<> OctreeIt(IrradiancePhotonMap); OctreeIt.HasPendingNodes(); OctreeIt.Advance()) { const FIrradiancePhotonOctree::FNode& CurrentNode = OctreeIt.GetCurrentNode(); const FOctreeNodeContext& CurrentContext = OctreeIt.GetCurrentContext(); // Push children onto the iterator stack if they intersect the query box if (!CurrentNode.IsLeaf()) { FOREACH_OCTREE_CHILD_NODE(ChildRef) { if (CurrentNode.HasChild(ChildRef)) { const FOctreeNodeContext ChildContext = CurrentContext.GetChildContext(ChildRef); if (ChildContext.Bounds.GetBox().Intersect(SearchBox)) { OctreeIt.PushChild(ChildRef); } } } } // Iterate over all photons in the nodes intersecting the query box for (FIrradiancePhotonOctree::ElementIt MeshIt(CurrentNode.GetElementIt()); MeshIt; ++MeshIt) { FIrradiancePhotonElement& PhotonElement = *MeshIt; FIrradiancePhoton& CurrentPhoton = PhotonElement.GetPhoton(); const FVector4f PhotonToVertexVector = Vertex.WorldPosition - CurrentPhoton.GetPosition(); const float DistanceSquared = PhotonToVertexVector.SizeSquared3(); const float CosTheta = Dot3(Vertex.WorldTangentZ, CurrentPhoton.GetSurfaceNormal()); // Only searching for irradiance photons with normals similar to the search normal if (CosTheta > PhotonMappingSettings.PhotonSearchAngleThreshold // And closer to the search position than the max search distance. && ((CurrentPhoton.HasDirectContribution() && (DistanceSquared < FMath::Square(PhotonMappingSettings.DirectPhotonSearchDistance))) || (!CurrentPhoton.HasDirectContribution() && (DistanceSquared < FMath::Square(PhotonMappingSettings.IndirectPhotonSearchDistance))))) { // Only accept irradiance photons within an angle of the plane defined by the vertex normal // This avoids expensive visibility traces to photons that are probably not on the same surface const float DirectionDotNormal = Dot3(CurrentPhoton.GetSurfaceNormal(), PhotonToVertexVector.GetSafeNormal()); if (FMath::Abs(DirectionDotNormal) < PhotonMappingSettings.MinCosIrradiancePhotonSearchCone) { if (bVisibleOnly) { // Store the photon for later, which is faster than tracing a ray here since this may not be the closest photon TempIrradiancePhotons.Add(&CurrentPhoton); } else if (DistanceSquared < ClosestDistanceSquared) { // Only accept the closest photon if visibility is not required ClosestPhoton = &CurrentPhoton; ClosestDistanceSquared = DistanceSquared; } } } } } } if (bVisibleOnly) { // Sort the photons so the closest photon is in the beginning of the array FCompareNearestIrradiancePhotons CompareClassInstance(Vertex.WorldPosition); TempIrradiancePhotons.Sort(CompareClassInstance); // Trace a ray from the vertex to each irradiance photon until a visible one is found, starting with the closest for (int32 PhotonIndex = 0; PhotonIndex < TempIrradiancePhotons.Num(); PhotonIndex++) { FIrradiancePhoton* CurrentPhoton = TempIrradiancePhotons[PhotonIndex]; const FVector4f VertexToPhoton = CurrentPhoton->GetPosition() - Vertex.WorldPosition; const FLightRay VertexToPhotonRay( Vertex.WorldPosition + VertexToPhoton.GetSafeNormal() * SceneConstants.VisibilityRayOffsetDistance + Vertex.WorldTangentZ * SceneConstants.VisibilityNormalOffsetDistance, CurrentPhoton->GetPosition() + CurrentPhoton->GetSurfaceNormal() * SceneConstants.VisibilityNormalOffsetDistance, NULL, NULL ); MappingContext.Stats.NumIrradiancePhotonSearchRays++; const float PreviousShadowTraceTime = MappingContext.RayCache.BooleanRayTraceTime; // Check the line segment for intersection with the static lighting meshes. FLightRayIntersection Intersection; AggregateMesh->IntersectLightRay(VertexToPhotonRay, false, false, false, MappingContext.RayCache, Intersection); MappingContext.Stats.IrradiancePhotonSearchRayTime += MappingContext.RayCache.BooleanRayTraceTime - PreviousShadowTraceTime; #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugThisLookup && PhotonMappingSettings.bVisualizePhotonGathers) { FDebugStaticLightingRay DebugRay(VertexToPhotonRay.Start, VertexToPhotonRay.End, Intersection.bIntersects); if (Intersection.bIntersects) { DebugRay.End = Intersection.IntersectionVertex.WorldPosition; } StartupDebugOutput.ShadowRays.Add(DebugRay); } #endif if (!Intersection.bIntersects) { // Break on the first visible photon ClosestPhoton = CurrentPhoton; break; } } } #if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING if (bDebugThisLookup && ClosestPhoton != NULL && PhotonMappingSettings.bVisualizePhotonGathers) { StartupDebugOutput.GatheredPhotons.Add(FDebugPhoton(0, ClosestPhoton->GetPosition(), ClosestPhoton->GetSurfaceNormal(), ClosestPhoton->GetSurfaceNormal())); } #endif return ClosestPhoton; } /** Calculates the irradiance for an irradiance photon */ FLinearColor FStaticLightingSystem::CalculatePhotonIrradiance( const FPhotonOctree& PhotonMap, int32 NumPhotonsEmitted, int32 NumPhotonsToFind, float SearchDistance, const FIrradiancePhoton& IrradiancePhoton, bool bDebugThisCalculation, TArray& TempFoundPhotons, FCalculateIrradiancePhotonStats& OutStats) const { // Empty TempFoundPhotons without causing any allocations / frees TempFoundPhotons.Empty(TempFoundPhotons.Num() + TempFoundPhotons.GetSlack()); const float MaxFoundDistanceSquared = FindNearbyPhotonsSorted( PhotonMap, IrradiancePhoton.GetPosition(), IrradiancePhoton.GetSurfaceNormal(), NumPhotonsToFind, SearchDistance, false, bDebugThisCalculation, TempFoundPhotons, OutStats); FLinearColor PhotonIrradiance(FLinearColor::Black); if (TempFoundPhotons.Num() > 0) { LIGHTINGSTAT(FScopedRDTSCTimer CalculateIrradianceTimer(OutStats.CalculateIrradianceThreadTime)); const float MaxFoundDistance = FMath::Sqrt(MaxFoundDistanceSquared); // Estimate the photon density using a cone filter, from the paper "Global Illumination using Photon Maps" const float DiskArea = (float)PI * MaxFoundDistanceSquared; const float ConeFilterNormalizeConstant = 1.0f - 2.0f / (3.0f * PhotonMappingSettings.ConeFilterConstant); const float ConstantWeight = 1.0f / (ConeFilterNormalizeConstant * NumPhotonsEmitted * DiskArea); FCoherentRayCache UnusedRayCache; for (int32 PhotonIndex = 0; PhotonIndex < TempFoundPhotons.Num(); PhotonIndex++) { const FPhoton& CurrentPhoton = TempFoundPhotons[PhotonIndex]; if (Dot3(IrradiancePhoton.GetSurfaceNormal(), CurrentPhoton.GetIncidentDirection()) > 0.0f) { float SearchNormalScales[2] = {.1f, .4f}; bool bPhotonVisible = false; // Try to determine visibility to the photon before letting it contribute // This helps to prevent leaking through thin walls for (int32 SearchIndex = 0; SearchIndex < UE_ARRAY_COUNT(SearchNormalScales) && !bPhotonVisible; SearchIndex++) { const float NormalOffset = SearchDistance * SearchNormalScales[SearchIndex]; const FLightRay Ray( IrradiancePhoton.GetPosition() + IrradiancePhoton.GetSurfaceNormal() * NormalOffset, CurrentPhoton.GetPosition() + CurrentPhoton.GetSurfaceNormal() * NormalOffset, NULL, NULL ); FLightRayIntersection RayIntersection; AggregateMesh->IntersectLightRay(Ray, false, false, false, UnusedRayCache, RayIntersection); bPhotonVisible = !RayIntersection.bIntersects; } if (bPhotonVisible) { const float PhotonDistance = (CurrentPhoton.GetPosition() - IrradiancePhoton.GetPosition()).Size3(); const float ConeWeight = FMath::Max(1.0f - PhotonDistance / (PhotonMappingSettings.ConeFilterConstant * MaxFoundDistance), 0.0f); PhotonIrradiance += CurrentPhoton.GetPower() * ConeWeight * ConstantWeight; } } } } return PhotonIrradiance; } /** Calculates incident radiance at a vertex from the given photon map. */ FGatheredLightSample FStaticLightingSystem::CalculatePhotonIncidentRadiance( const FPhotonOctree& PhotonMap, int32 NumPhotonsEmitted, float SearchDistance, const FStaticLightingVertex& Vertex, bool bDebugThisDensityEstimation) const { TArray FoundPhotons; FFindNearbyPhotonStats DummyStats; FindNearbyPhotonsIterative(PhotonMap, Vertex.WorldPosition, Vertex.WorldTangentZ, 1, SearchDistance, SearchDistance, bDebugThisDensityEstimation, false, FoundPhotons, DummyStats); FGatheredLightSample PhotonIncidentRadiance; if (FoundPhotons.Num() > 0) { // Estimate the photon density using a cone filter, from the paper "Global Illumination using Photon Maps" const float DiskArea = (float)PI * SearchDistance * SearchDistance; const float ConeFilterNormalizeConstant = 1.0f - 2.0f / (3.0f * PhotonMappingSettings.ConeFilterConstant); const float ConstantWeight = 1.0f / (ConeFilterNormalizeConstant * NumPhotonsEmitted * DiskArea); for (int32 PhotonIndex = 0; PhotonIndex < FoundPhotons.Num(); PhotonIndex++) { const FPhoton& CurrentPhoton = FoundPhotons[PhotonIndex]; const FVector4f TangentPathDirection = Vertex.TransformWorldVectorToTangent(CurrentPhoton.GetIncidentDirection()); if (TangentPathDirection.Z > 0) { const float PhotonDistance = (CurrentPhoton.GetPosition() - Vertex.WorldPosition).Size3(); const float ConeWeight = FMath::Max(1.0f - PhotonDistance / (PhotonMappingSettings.ConeFilterConstant * SearchDistance), 0.0f); PhotonIncidentRadiance.AddWeighted(FGatheredLightSampleUtil::PointLightWorldSpace<2>(CurrentPhoton.GetPower(), TangentPathDirection, CurrentPhoton.GetIncidentDirection()), ConeWeight * ConstantWeight); } } } return PhotonIncidentRadiance; } /** Calculates exitant radiance at a vertex from the given photon map. */ FLinearColor FStaticLightingSystem::CalculatePhotonExitantRadiance( const FPhotonOctree& PhotonMap, int32 NumPhotonsEmitted, float SearchDistance, const FStaticLightingMesh* Mesh, const FMinimalStaticLightingVertex& Vertex, int32 ElementIndex, const FVector4f& OutgoingDirection, bool bDebugThisDensityEstimation) const { TArray FoundPhotons; FFindNearbyPhotonStats DummyStats; FindNearbyPhotonsIterative(PhotonMap, Vertex.WorldPosition, Vertex.WorldTangentZ, 1, SearchDistance, SearchDistance, bDebugThisDensityEstimation, false, FoundPhotons, DummyStats); FLinearColor AccumulatedRadiance(FLinearColor::Black); if (FoundPhotons.Num() > 0) { // Estimate the photon density using a cone filter, from the paper "Global Illumination using Photon Maps" const float DiskArea = (float)PI * SearchDistance * SearchDistance; const float ConeFilterNormalizeConstant = 1.0f - 2.0f / (3.0f * PhotonMappingSettings.ConeFilterConstant); const float ConstantWeight = 1.0f / (ConeFilterNormalizeConstant * NumPhotonsEmitted * DiskArea); for (int32 PhotonIndex = 0; PhotonIndex < FoundPhotons.Num(); PhotonIndex++) { const FPhoton& CurrentPhoton = FoundPhotons[PhotonIndex]; if (Dot3(Vertex.WorldTangentZ, CurrentPhoton.GetIncidentDirection()) > 0.0f) { const float PhotonDistance = (CurrentPhoton.GetPosition() - Vertex.WorldPosition).Size3(); const float ConeWeight = FMath::Max(1.0f - PhotonDistance / (PhotonMappingSettings.ConeFilterConstant * SearchDistance), 0.0f); const FLinearColor BRDF = Mesh->EvaluateBRDF(Vertex, ElementIndex, CurrentPhoton.GetIncidentDirection(), OutgoingDirection); AccumulatedRadiance += CurrentPhoton.GetPower() * ConeWeight * ConstantWeight * BRDF; } } } return AccumulatedRadiance; } }