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

2720 lines
122 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LightingSystem.h"
#include "Exporter.h"
#include "LightmassSwarm.h"
#include "CPUSolver.h"
#include "MonteCarlo.h"
#include "Misc/ScopeLock.h"
#include "UnrealLightmass.h"
#include "HAL/RunnableThread.h"
#include "HAL/PlatformProcess.h"
#include "Misc/OutputDeviceRedirector.h"
#include "HAL/ExceptionHandling.h"
#if USE_LOCAL_SWARM_INTERFACE
#include "IMessagingModule.h"
#include "Async/TaskGraphInterfaces.h"
#endif
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include <psapi.h>
#include "Windows/HideWindowsPlatformTypes.h"
#pragma comment(lib, "psapi.lib")
#endif
namespace Lightmass
{
static void ConvertToLightSampleHelper(const FGatheredLightSample& InGatheredLightSample, float OutCoefficients[2][3])
{
// SHCorrection is SHVector sampled with the normal
float DirCorrection = 1.0f / FMath::Max( 0.0001f, InGatheredLightSample.SHCorrection );
float DirLuma[4];
for( int32 i = 0; i < 4; i++ )
{
DirLuma[i] = 0.30f * InGatheredLightSample.SHVector.R.V[i];
DirLuma[i] += 0.59f * InGatheredLightSample.SHVector.G.V[i];
DirLuma[i] += 0.11f * InGatheredLightSample.SHVector.B.V[i];
// Lighting is already in IncidentLighting. Force directional SH as applied to a flat normal map to be 1 to get purely directional data.
DirLuma[i] *= DirCorrection / PI;
}
// Scale directionality so that DirLuma[0] == 1. Then scale color to compensate and toss DirLuma[0].
float DirScale = 1.0f / FMath::Max( 0.0001f, DirLuma[0] );
float ColorScale = DirLuma[0];
// IncidentLighting is ground truth for a representative direction, the vertex normal
OutCoefficients[0][0] = ColorScale * InGatheredLightSample.IncidentLighting.R;
OutCoefficients[0][1] = ColorScale * InGatheredLightSample.IncidentLighting.G;
OutCoefficients[0][2] = ColorScale * InGatheredLightSample.IncidentLighting.B;
// Will force DirLuma[0] to 0.282095f
OutCoefficients[1][0] = -0.325735f * DirLuma[1] * DirScale;
OutCoefficients[1][1] = 0.325735f * DirLuma[2] * DirScale;
OutCoefficients[1][2] = -0.325735f * DirLuma[3] * DirScale;
}
FLightSample FGatheredLightMapSample::ConvertToLightSample(bool bDebugThisSample) const
{
FLightSample NewSample;
NewSample.bIsMapped = bIsMapped;
ConvertToLightSampleHelper(HighQuality, &NewSample.Coefficients[0]);
ConvertToLightSampleHelper(LowQuality, &NewSample.Coefficients[ LM_LQ_LIGHTMAP_COEF_INDEX ]);
NewSample.SkyOcclusion[0] = HighQuality.SkyOcclusion.X;
NewSample.SkyOcclusion[1] = HighQuality.SkyOcclusion.Y;
NewSample.SkyOcclusion[2] = HighQuality.SkyOcclusion.Z;
NewSample.AOMaterialMask = HighQuality.AOMaterialMask;
return NewSample;
}
FLightMapData2D* FGatheredLightMapData2D::ConvertToLightmap2D(bool bDebugThisMapping, int32 PaddedDebugX, int32 PaddedDebugY) const
{
FLightMapData2D* ConvertedLightMap = new FLightMapData2D(SizeX, SizeY);
ConvertedLightMap->Lights = Lights;
ConvertedLightMap->bHasSkyShadowing = bHasSkyShadowing;
for (int32 SampleIndex = 0; SampleIndex < Data.Num(); SampleIndex++)
{
const bool bDebugThisSample = bDebugThisMapping && SampleIndex == PaddedDebugY * SizeX + PaddedDebugX;
(*ConvertedLightMap)(SampleIndex, 0) = Data[SampleIndex].ConvertToLightSample(bDebugThisSample);
}
return ConvertedLightMap;
}
FStaticLightingMappingContext::FStaticLightingMappingContext(const FStaticLightingMesh* InSubjectMesh, FStaticLightingSystem& InSystem, FDebugLightingOutput* InDebugOutput) :
FirstBounceCache(InSubjectMesh ? InSubjectMesh->BoundingBox : FBox3f::BuildAABB(FVector4f(0,0,0), FVector4f(HALF_WORLD_MAX)), InSystem, 1),
System(InSystem),
DebugOutput(InDebugOutput)
{}
FStaticLightingMappingContext::~FStaticLightingMappingContext()
{
{
// Update the main threads stats with the stats from this mapping
FScopeLock Lock(&System.Stats.StatsSync);
System.Stats.Cache[0] += FirstBounceCache.Stats;
System.Stats += Stats;
System.Stats.NumFirstHitRaysTraced += RayCache.NumFirstHitRaysTraced;
System.Stats.NumBooleanRaysTraced += RayCache.NumBooleanRaysTraced;
System.Stats.FirstHitRayTraceThreadTime += RayCache.FirstHitRayTraceTime;
System.Stats.BooleanRayTraceThreadTime += RayCache.BooleanRayTraceTime;
}
for (int32 EntryIndex = 0; EntryIndex < RefinementTreeFreePool.Num(); EntryIndex++)
{
// Delete on the main thread to avoid a TBB inefficiency deleting many same-sized allocations on different threads
delete RefinementTreeFreePool[EntryIndex];
}
}
/**
* Initializes this static lighting system, and builds static lighting based on the provided options.
* @param InOptions - The static lighting build options.
* @param InScene - The scene containing all the lights and meshes
* @param InExporter - The exporter used to send completed data back to UE5
* @param InNumThreads - Number of concurrent threads to use for lighting building
*/
FStaticLightingSystem::FStaticLightingSystem(const FLightingBuildOptions& InOptions, FScene& InScene, FLightmassSolverExporter& InExporter, int32 InNumThreads)
: Options(InOptions)
, GeneralSettings(InScene.GeneralSettings)
, SceneConstants(InScene.SceneConstants)
, MaterialSettings(InScene.MaterialSettings)
, MeshAreaLightSettings(InScene.MeshAreaLightSettings)
, DynamicObjectSettings(InScene.DynamicObjectSettings)
, VolumetricLightmapSettings(InScene.VolumetricLightmapSettings)
, PrecomputedVisibilitySettings(InScene.PrecomputedVisibilitySettings)
, VolumeDistanceFieldSettings(InScene.VolumeDistanceFieldSettings)
, AmbientOcclusionSettings(InScene.AmbientOcclusionSettings)
, ShadowSettings(InScene.ShadowSettings)
, ImportanceTracingSettings(InScene.ImportanceTracingSettings)
, PhotonMappingSettings(InScene.PhotonMappingSettings)
, IrradianceCachingSettings(InScene.IrradianceCachingSettings)
, TasksInProgressThatWillNeedHelp(0)
, NextVolumeSampleTaskIndex(-1)
, NumVolumeSampleTasksOutstanding(0)
, bShouldExportVolumeSampleData(false)
, VolumeLightingInterpolationOctree(FVector4f(0,0,0), HALF_WORLD_MAX)
, bShouldExportMeshAreaLightData(false)
, bShouldExportVolumeDistanceField(false)
, NumPhotonsEmittedDirect(0)
, DirectPhotonMap(FVector4f(0,0,0), HALF_WORLD_MAX)
, NumPhotonsEmittedFirstBounce(0)
, FirstBouncePhotonMap(FVector4f(0,0,0), HALF_WORLD_MAX)
, FirstBounceEscapedPhotonMap(FVector4f(0,0,0), HALF_WORLD_MAX)
, FirstBouncePhotonSegmentMap(FVector4f(0,0,0), HALF_WORLD_MAX)
, NumPhotonsEmittedSecondBounce(0)
, SecondBouncePhotonMap(FVector4f(0,0,0), HALF_WORLD_MAX)
, IrradiancePhotonMap(FVector4f(0,0,0), HALF_WORLD_MAX)
, AggregateMesh(NULL)
, VoxelizationSurfaceAggregateMesh(NULL)
, VoxelizationVolumeAggregateMesh(NULL)
, LandscapeCullingVoxelizationAggregateMesh(NULL)
, Scene(InScene)
, NumTexelsCompleted(0)
, NumOutstandingVolumeDataLayers(0)
, OutstandingVolumeDataLayerIndex(-1)
, NumStaticLightingThreads(InScene.GeneralSettings.bAllowMultiThreadedStaticLighting ? FMath::Max(InNumThreads, 1) : 1)
, DebugIrradiancePhotonCalculationArrayIndex(INDEX_NONE)
, DebugIrradiancePhotonCalculationPhotonIndex(INDEX_NONE)
, Exporter(InExporter)
{
const double SceneSetupStart = FPlatformTime::Seconds();
UE_LOG(LogLightmass, Log, TEXT("FStaticLightingSystem started using GKDOPMaxTrisPerLeaf: %d"), DEFAULT_MAX_TRIS_PER_LEAF);
ValidateSettings(InScene);
bool bDumpAllMappings = false;
GroupVisibilityGridSizeXY = 0;
GroupVisibilityGridSizeZ = 0;
// Pre-allocate containers.
int32 NumMeshes = 0;
int32 NumVertices = 0;
int32 NumTriangles = 0;
int32 NumMappings = InScene.TextureLightingMappings.Num() +
InScene.FluidMappings.Num() + InScene.LandscapeMappings.Num() + InScene.BspMappings.Num() + InScene.LandscapeVolumeMappings.Num();
int32 NumMeshInstances = InScene.BspMappings.Num() + InScene.StaticMeshInstances.Num() + InScene.VolumeMappings.Num();
AllMappings.Reserve( NumMappings );
Meshes.Reserve( NumMeshInstances );
// Initialize Meshes, Mappings, AllMappings and AggregateMesh from the scene
UE_LOG(LogLightmass, Log, TEXT("Number of texture mappings: %d"), InScene.TextureLightingMappings.Num() );
for (int32 MappingIndex = 0; MappingIndex < InScene.TextureLightingMappings.Num(); MappingIndex++)
{
FStaticMeshStaticLightingTextureMapping* Mapping = &InScene.TextureLightingMappings[MappingIndex];
Mappings.Add(Mapping->Guid, Mapping);
AllMappings.Add(Mapping);
if (bDumpAllMappings)
{
UE_LOG(LogLightmass, Log, TEXT("\t%s"), *(Mapping->Guid.ToString()));
}
}
UE_LOG(LogLightmass, Log, TEXT("Number of fluid mappings: %d"), InScene.FluidMappings.Num());
for (int32 MappingIndex = 0; MappingIndex < InScene.FluidMappings.Num(); MappingIndex++)
{
FFluidSurfaceStaticLightingTextureMapping* Mapping = &InScene.FluidMappings[MappingIndex];
NumMeshes++;
NumVertices += Mapping->Mesh->NumVertices;
NumTriangles += Mapping->Mesh->NumTriangles;
Mappings.Add(Mapping->Guid, Mapping);
AllMappings.Add(Mapping);
if (bDumpAllMappings)
{
UE_LOG(LogLightmass, Log, TEXT("\t%s"), *(Mapping->Guid.ToString()));
}
}
for (int32 MeshIndex = 0; MeshIndex < InScene.FluidMeshInstances.Num(); MeshIndex++)
{
Meshes.Add(&InScene.FluidMeshInstances[MeshIndex]);
}
UE_LOG(LogLightmass, Log, TEXT("Number of landscape mappings: %d"), InScene.LandscapeMappings.Num());
for (int32 MappingIndex = 0; MappingIndex < InScene.LandscapeMappings.Num(); MappingIndex++)
{
FLandscapeStaticLightingTextureMapping* Mapping = &InScene.LandscapeMappings[MappingIndex];
NumMeshes++;
NumVertices += Mapping->Mesh->NumVertices;
NumTriangles += Mapping->Mesh->NumTriangles;
Mappings.Add(Mapping->Guid, Mapping);
LandscapeMappings.Add(Mapping);
AllMappings.Add(Mapping);
if (bDumpAllMappings)
{
UE_LOG(LogLightmass, Log, TEXT("\t%s"), *(Mapping->Guid.ToString()));
}
}
for (int32 MeshIndex = 0; MeshIndex < InScene.LandscapeMeshInstances.Num(); MeshIndex++)
{
Meshes.Add(&InScene.LandscapeMeshInstances[MeshIndex]);
}
UE_LOG(LogLightmass, Log, TEXT("Number of BSP mappings: %d"), InScene.BspMappings.Num() );
for( int32 MeshIdx=0; MeshIdx < InScene.BspMappings.Num(); ++MeshIdx )
{
FBSPSurfaceStaticLighting* BSPMapping = &InScene.BspMappings[MeshIdx];
Meshes.Add(BSPMapping);
NumMeshes++;
NumVertices += BSPMapping->NumVertices;
NumTriangles += BSPMapping->NumTriangles;
// add the BSP mappings light mapping object
AllMappings.Add(&BSPMapping->Mapping);
Mappings.Add(BSPMapping->Mapping.Guid, &BSPMapping->Mapping);
if (bDumpAllMappings)
{
UE_LOG(LogLightmass, Log, TEXT("\t%s"), *(BSPMapping->Mapping.Guid.ToString()));
}
}
UE_LOG(LogLightmass, Log, TEXT("Number of volume mappings: %d"), InScene.VolumeMappings.Num());
for (int32 MappingIndex = 0; MappingIndex < InScene.VolumeMappings.Num(); MappingIndex++)
{
FStaticLightingGlobalVolumeMapping* Mapping = &InScene.VolumeMappings[MappingIndex];
Mappings.Add(Mapping->Guid, Mapping);
AllMappings.Add(Mapping);
if (bDumpAllMappings)
{
UE_LOG(LogLightmass, Log, TEXT("\t%s"), *(Mapping->Guid.ToString()));
}
}
UE_LOG(LogLightmass, Log, TEXT("Number of landscape volume mappings: %d"), InScene.LandscapeVolumeMappings.Num());
for (int32 MappingIndex = 0; MappingIndex < InScene.LandscapeVolumeMappings.Num(); MappingIndex++)
{
FLandscapeStaticLightingGlobalVolumeMapping* Mapping = &InScene.LandscapeVolumeMappings[MappingIndex];
Mappings.Add(Mapping->Guid, Mapping);
AllMappings.Add(Mapping);
LandscapeMappings.Add(Mapping);
if (bDumpAllMappings)
{
UE_LOG(LogLightmass, Log, TEXT("\t%s"), *(Mapping->Guid.ToString()));
}
}
UE_LOG(LogLightmass, Log, TEXT("Number of static mesh instance mappings: %d"), InScene.StaticMeshInstances.Num() );
for (int32 MeshIndex = 0; MeshIndex < InScene.StaticMeshInstances.Num(); MeshIndex++)
{
FStaticMeshStaticLightingMesh* MeshInstance = &InScene.StaticMeshInstances[MeshIndex];
FStaticLightingMapping** MappingPtr = Mappings.Find(MeshInstance->Guid);
if (MappingPtr != NULL)
{
MeshInstance->Mapping = *MappingPtr;
}
else
{
MeshInstance->Mapping = NULL;
}
Meshes.Add(MeshInstance);
NumMeshes++;
NumVertices += MeshInstance->NumVertices;
NumTriangles += MeshInstance->NumTriangles;
}
check(Meshes.Num() == AllMappings.Num());
int32 MaxVisibilityId = -1;
for (int32 MeshIndex = 0; MeshIndex < Meshes.Num(); MeshIndex++)
{
const FStaticLightingMesh* Mesh = Meshes[MeshIndex];
for (int32 VisIndex = 0; VisIndex < Mesh->VisibilityIds.Num(); VisIndex++)
{
MaxVisibilityId = FMath::Max(MaxVisibilityId, Mesh->VisibilityIds[VisIndex]);
}
}
VisibilityMeshes.Empty(MaxVisibilityId + 1);
VisibilityMeshes.AddZeroed(MaxVisibilityId + 1);
for (int32 MeshIndex = 0; MeshIndex < Meshes.Num(); MeshIndex++)
{
FStaticLightingMesh* Mesh = Meshes[MeshIndex];
for (int32 VisIndex = 0; VisIndex < Mesh->VisibilityIds.Num(); VisIndex++)
{
const int32 VisibilityId = Mesh->VisibilityIds[VisIndex];
if (VisibilityId >= 0)
{
VisibilityMeshes[VisibilityId].Meshes.AddUnique(Mesh);
}
}
}
for (int32 VisibilityMeshIndex = 0; VisibilityMeshIndex < VisibilityMeshes.Num(); VisibilityMeshIndex++)
{
checkSlow(VisibilityMeshes[VisibilityMeshIndex].Meshes.Num() > 0);
}
{
FScopedRDTSCTimer MeshSetupTimer(Stats.MeshAreaLightSetupTime);
for (int32 MeshIndex = 0; MeshIndex < Meshes.Num(); MeshIndex++)
{
const int32 BckNumMeshAreaLights = MeshAreaLights.Num();
// Create mesh area lights from each mesh
Meshes[MeshIndex]->CreateMeshAreaLights(*this, Scene, MeshAreaLights);
if (MeshAreaLights.Num() > BckNumMeshAreaLights)
{
Stats.NumMeshAreaLightMeshes++;
}
Meshes[MeshIndex]->SetDebugMaterial(MaterialSettings.bUseDebugMaterial, MaterialSettings.DebugDiffuse);
}
}
for (int32 MeshIndex = 0; MeshIndex < Meshes.Num(); MeshIndex++)
{
for (int32 LightIndex = 0; LightIndex < MeshAreaLights.Num(); LightIndex++)
{
// Register the newly created mesh area lights with every relevant mesh so they are used for lighting
if (MeshAreaLights[LightIndex].AffectsBounds(FBoxSphereBounds3f(Meshes[MeshIndex]->BoundingBox)))
{
Meshes[MeshIndex]->RelevantLights.Add(&MeshAreaLights[LightIndex]);
}
}
}
#if USE_EMBREE
if (Scene.EmbreeDevice)
{
if (Scene.bVerifyEmbree)
{
AggregateMesh = new FEmbreeVerifyAggregateMesh(Scene);
}
else
{
AggregateMesh = new FEmbreeAggregateMesh(Scene);
}
}
else
#endif
{
AggregateMesh = new FDefaultAggregateMesh(Scene);
}
// Add all meshes to the kDOP.
AggregateMesh->ReserveMemory(NumMeshes, NumVertices, NumTriangles);
if (Scene.GeneralSettings.bUseFastVoxelization)
{
VoxelizationSurfaceAggregateMesh = new FDefaultAggregateMesh(Scene);
VoxelizationVolumeAggregateMesh = new FDefaultAggregateMesh(Scene);
LandscapeCullingVoxelizationAggregateMesh = new FDefaultAggregateMesh(Scene);
}
for (int32 MappingIndex = 0; MappingIndex < InScene.FluidMappings.Num(); MappingIndex++)
{
FFluidSurfaceStaticLightingTextureMapping* Mapping = &InScene.FluidMappings[MappingIndex];
AggregateMesh->AddMesh(Mapping->Mesh, Mapping);
if (Scene.GeneralSettings.bUseFastVoxelization)
{
if (Mapping->GetVolumeMapping() == nullptr)
{
VoxelizationSurfaceAggregateMesh->AddMeshForVoxelization(Mapping->Mesh, Mapping);
}
else
{
VoxelizationVolumeAggregateMesh->AddMeshForVoxelization(Mapping->Mesh, Mapping);
}
}
}
for (int32 MappingIndex = 0; MappingIndex < LandscapeMappings.Num(); MappingIndex++)
{
FStaticLightingMapping* Mapping = LandscapeMappings[MappingIndex];
AggregateMesh->AddMesh(Mapping->Mesh, Mapping);
if (Scene.GeneralSettings.bUseFastVoxelization)
{
if (Mapping->GetLandscapeVolumeMapping() == nullptr)
{
VoxelizationSurfaceAggregateMesh->AddMeshForVoxelization(Mapping->Mesh, Mapping);
}
else
{
VoxelizationVolumeAggregateMesh->AddMeshForVoxelization(Mapping->Mesh, Mapping);
}
LandscapeCullingVoxelizationAggregateMesh->AddMeshForVoxelization(Mapping->Mesh, Mapping);
}
}
for( int32 MeshIdx=0; MeshIdx < InScene.BspMappings.Num(); ++MeshIdx )
{
FBSPSurfaceStaticLighting* BSPMapping = &InScene.BspMappings[MeshIdx];
AggregateMesh->AddMesh(BSPMapping, &BSPMapping->Mapping);
if (Scene.GeneralSettings.bUseFastVoxelization)
{
if (BSPMapping->Mapping.GetVolumeMapping() == nullptr)
{
VoxelizationSurfaceAggregateMesh->AddMeshForVoxelization(BSPMapping, &BSPMapping->Mapping);
}
else
{
VoxelizationVolumeAggregateMesh->AddMeshForVoxelization(BSPMapping, &BSPMapping->Mapping);
}
}
}
for (int32 MeshIndex = 0; MeshIndex < InScene.StaticMeshInstances.Num(); MeshIndex++)
{
FStaticMeshStaticLightingMesh* MeshInstance = &InScene.StaticMeshInstances[MeshIndex];
AggregateMesh->AddMesh(MeshInstance, MeshInstance->Mapping);
if (Scene.GeneralSettings.bUseFastVoxelization)
{
if (MeshInstance->GetInstanceableStaticMesh() != nullptr)
{
if (MeshInstance->StaticMesh->VoxelizationMesh == nullptr)
{
if (MeshInstance->LightingFlags & GI_INSTANCE_CASTSHADOW && MeshInstance->DoesMeshBelongToLOD0())
{
MeshInstance->StaticMesh->VoxelizationMesh = new FDefaultAggregateMesh(Scene);
MeshInstance->StaticMesh->VoxelizationMesh->AddMeshForVoxelization(MeshInstance, MeshInstance->Mapping, true);
MeshInstance->StaticMesh->VoxelizationMesh->PrepareForRaytracing();
}
}
}
else
{
// For non-instanceable static meshes (splines), add them to the aggregate scene voxelization mesh
if (MeshInstance->Mapping->GetVolumeMapping() == nullptr)
{
VoxelizationSurfaceAggregateMesh->AddMeshForVoxelization(MeshInstance, MeshInstance->Mapping);
}
else
{
VoxelizationVolumeAggregateMesh->AddMeshForVoxelization(MeshInstance, MeshInstance->Mapping);
}
}
}
}
// Comparing mappings based on cost, descending.
struct FCompareProcessingCost
{
FORCEINLINE bool operator()( const FStaticLightingMapping& A, const FStaticLightingMapping& B ) const
{
return B.GetProcessingCost() < A.GetProcessingCost();
}
};
// Sort mappings by processing cost, descending.
Mappings.ValueSort(FCompareProcessingCost());
AllMappings.Sort(FCompareProcessingCost());
GStatistics.NumTotalMappings = Mappings.Num();
const FBoxSphereBounds3f SceneBounds = FBoxSphereBounds3f(AggregateMesh->GetBounds());
const FBoxSphereBounds3f ImportanceBounds = GetImportanceBounds();
// Never trace further than the importance or scene diameter
MaxRayDistance = ImportanceBounds.SphereRadius > 0.0f ? ImportanceBounds.SphereRadius * 2.0f : SceneBounds.SphereRadius * 2.0f;
Stats.NumLights = InScene.DirectionalLights.Num() + InScene.PointLights.Num() + InScene.SpotLights.Num() + InScene.RectLights.Num() + MeshAreaLights.Num();
Stats.NumMeshAreaLights = MeshAreaLights.Num();
for (int32 i = 0; i < MeshAreaLights.Num(); i++)
{
Stats.NumMeshAreaLightPrimitives += MeshAreaLights[i].GetNumPrimitives();
Stats.NumSimplifiedMeshAreaLightPrimitives += MeshAreaLights[i].GetNumSimplifiedPrimitives();
}
// Add all light types except sky lights to the system's Lights array
Lights.Reserve(Stats.NumLights);
for (int32 LightIndex = 0; LightIndex < InScene.DirectionalLights.Num(); LightIndex++)
{
InScene.DirectionalLights[LightIndex].Initialize(
SceneBounds,
PhotonMappingSettings.bEmitPhotonsOutsideImportanceVolume,
ImportanceBounds,
Scene.PhotonMappingSettings.IndirectPhotonEmitDiskRadius,
Scene.SceneConstants.LightGridSize,
Scene.PhotonMappingSettings.DirectPhotonDensity,
Scene.PhotonMappingSettings.DirectPhotonDensity * Scene.PhotonMappingSettings.OutsideImportanceVolumeDensityScale);
Lights.Add(&InScene.DirectionalLights[LightIndex]);
}
// Initialize lights and add them to the solver's Lights array
for (int32 LightIndex = 0; LightIndex < InScene.PointLights.Num(); LightIndex++)
{
InScene.PointLights[LightIndex].Initialize(Scene.PhotonMappingSettings.IndirectPhotonEmitConeAngle);
Lights.Add(&InScene.PointLights[LightIndex]);
}
for (int32 LightIndex = 0; LightIndex < InScene.SpotLights.Num(); LightIndex++)
{
InScene.SpotLights[LightIndex].Initialize(Scene.PhotonMappingSettings.IndirectPhotonEmitConeAngle);
Lights.Add(&InScene.SpotLights[LightIndex]);
}
for (int32 LightIndex = 0; LightIndex < InScene.RectLights.Num(); LightIndex++)
{
InScene.RectLights[LightIndex].Initialize(Scene.PhotonMappingSettings.IndirectPhotonEmitConeAngle);
Lights.Add(&InScene.RectLights[LightIndex]);
}
const FBoxSphereBounds3f EffectiveImportanceBounds = ImportanceBounds.SphereRadius > 0.0f ? ImportanceBounds : SceneBounds;
for (int32 LightIndex = 0; LightIndex < MeshAreaLights.Num(); LightIndex++)
{
MeshAreaLights[LightIndex].Initialize(Scene.PhotonMappingSettings.IndirectPhotonEmitConeAngle, EffectiveImportanceBounds);
Lights.Add(&MeshAreaLights[LightIndex]);
}
for (int32 LightIndex = 0; LightIndex < InScene.SkyLights.Num(); LightIndex++)
{
SkyLights.Add(&InScene.SkyLights[LightIndex]);
}
//@todo - only count mappings being built
Stats.NumMappings = AllMappings.Num();
for (int32 MappingIndex = 0; MappingIndex < AllMappings.Num(); MappingIndex++)
{
FStaticLightingTextureMapping* TextureMapping = AllMappings[MappingIndex]->GetTextureMapping();
if (TextureMapping && !(AllMappings[MappingIndex]->GetVolumeMapping() || AllMappings[MappingIndex]->GetLandscapeVolumeMapping()))
{
Stats.NumTexelsProcessed += TextureMapping->CachedSizeX * TextureMapping->CachedSizeY;
}
AllMappings[MappingIndex]->SceneMappingIndex = MappingIndex;
AllMappings[MappingIndex]->Initialize(*this);
}
InitializePhotonSettings();
// Prepare the aggregate mesh for raytracing.
AggregateMesh->PrepareForRaytracing();
AggregateMesh->DumpStats();
if (Scene.GeneralSettings.bUseFastVoxelization)
{
VoxelizationSurfaceAggregateMesh->PrepareForRaytracing();
VoxelizationVolumeAggregateMesh->PrepareForRaytracing();
LandscapeCullingVoxelizationAggregateMesh->PrepareForRaytracing();
}
NumCompletedRadiosityIterationMappings.Empty(GeneralSettings.NumSkyLightingBounces);
NumCompletedRadiosityIterationMappings.AddDefaulted(GeneralSettings.NumSkyLightingBounces);
Stats.SceneSetupTime = FPlatformTime::Seconds() - SceneSetupStart;
GStatistics.SceneSetupTime += Stats.SceneSetupTime;
// spread out the work over multiple parallel threads
MultithreadProcess();
}
FStaticLightingSystem::~FStaticLightingSystem()
{
delete AggregateMesh;
AggregateMesh = NULL;
if (Scene.GeneralSettings.bUseFastVoxelization)
{
delete VoxelizationSurfaceAggregateMesh;
VoxelizationSurfaceAggregateMesh = NULL;
delete VoxelizationVolumeAggregateMesh;
VoxelizationVolumeAggregateMesh = NULL;
for (int32 MeshIndex = 0; MeshIndex < Scene.StaticMeshInstances.Num(); MeshIndex++)
{
const FStaticMeshStaticLightingMesh* MeshInstance = &Scene.StaticMeshInstances[MeshIndex];
if (MeshInstance->StaticMesh->VoxelizationMesh != nullptr)
{
delete MeshInstance->StaticMesh->VoxelizationMesh;
MeshInstance->StaticMesh->VoxelizationMesh = nullptr;
}
}
delete LandscapeCullingVoxelizationAggregateMesh;
LandscapeCullingVoxelizationAggregateMesh = NULL;
}
}
/**
* Creates multiple worker threads and starts the process locally.
*/
void FStaticLightingSystem::MultithreadProcess()
{
const double StartTime = FPlatformTime::Seconds();
UE_LOG(LogLightmass, Log, TEXT("Processing...") );
GStatistics.PhotonsStart = FPlatformTime::Seconds();
CacheSamples();
if (PhotonMappingSettings.bUsePhotonMapping)
{
// Build photon maps
EmitPhotons();
}
if (ImportanceTracingSettings.bUseRadiositySolverForSkylightMultibounce)
{
SetupRadiosity();
RunRadiosityIterations();
}
FinalizeSurfaceCache();
if (DynamicObjectSettings.bVisualizeVolumeLightInterpolation)
{
// Calculate volume samples now if they will be needed by the lighting threads for shading,
// Otherwise the volume samples will be calculated when the task is received from swarm.
BeginCalculateVolumeSamples();
}
SetupPrecomputedVisibility();
// Before we spawn the static lighting threads, prefetch tasks they'll be working on
GSwarm->PrefetchTasks();
GStatistics.PhotonsEnd = GStatistics.WorkTimeStart = FPlatformTime::Seconds();
const double SequentialThreadedProcessingStart = FPlatformTime::Seconds();
// Spawn the static lighting threads.
for(int32 ThreadIndex = 0;ThreadIndex < NumStaticLightingThreads;ThreadIndex++)
{
FMappingProcessingThreadRunnable* ThreadRunnable = new FMappingProcessingThreadRunnable(this, ThreadIndex, StaticLightingTask_ProcessMappings);
Threads.Add(ThreadRunnable);
const FString ThreadName = FString::Printf(TEXT("MappingProcessingThread%u"), ThreadIndex);
ThreadRunnable->Thread = FRunnableThread::Create(ThreadRunnable, *ThreadName);
}
GStatistics.NumThreads = NumStaticLightingThreads + 1; // Includes the main-thread who is only exporting.
// Stop the static lighting threads.
double MaxThreadTime = GStatistics.ThreadStatistics.TotalTime;
float MaxThreadBusyTime = 0;
int32 NumStaticLightingThreadsDone = 0;
while ( NumStaticLightingThreadsDone < NumStaticLightingThreads )
{
for (int32 ThreadIndex = 0; ThreadIndex < Threads.Num(); ThreadIndex++ )
{
if ( Threads[ThreadIndex].Thread != NULL )
{
// Check to see if the thread has exited with an error
if ( Threads[ThreadIndex].CheckHealth( true ) )
{
// Wait for the thread to exit
if (Threads[ThreadIndex].IsComplete())
{
Threads[ThreadIndex].Thread->WaitForCompletion();
// Accumulate all thread statistics
GStatistics.ThreadStatistics += Threads[ThreadIndex].ThreadStatistics;
MaxThreadTime = FMath::Max<double>(MaxThreadTime, Threads[ThreadIndex].ThreadStatistics.TotalTime);
if ( GReportDetailedStats )
{
UE_LOG(LogLightmass, Log, TEXT("Thread %d finished: %s"), ThreadIndex, *FPlatformTime::PrettyTime(Threads[ThreadIndex].ThreadStatistics.TotalTime) );
}
MaxThreadBusyTime = FMath::Max(MaxThreadBusyTime, Threads[ThreadIndex].ExecutionTime - Threads[ThreadIndex].IdleTime);
Stats.TotalLightingThreadTime += Threads[ThreadIndex].ExecutionTime - Threads[ThreadIndex].IdleTime;
// We're done with the thread object, destroy it
delete Threads[ThreadIndex].Thread;
Threads[ThreadIndex].Thread = NULL;
NumStaticLightingThreadsDone++;
}
else
{
FPlatformProcess::Sleep(0.01f);
}
}
}
}
// Try to do some mappings while we're waiting for threads to finish
if ( NumStaticLightingThreadsDone < NumStaticLightingThreads )
{
CompleteTextureMappingList.ApplyAndClear( *this );
ExportNonMappingTasks();
}
#if USE_LOCAL_SWARM_INTERFACE
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
#endif
GLog->FlushThreadedLogs();
}
Threads.Empty();
GStatistics.WorkTimeEnd = FPlatformTime::Seconds();
// Threads will idle when they have no more tasks but before the user accepts the async build changes, so we have to make sure we only count busy time
Stats.MainThreadLightingTime = (SequentialThreadedProcessingStart - StartTime) + MaxThreadBusyTime;
GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_ExportingResults, -1 ) );
// Apply any outstanding completed mappings.
CompleteTextureMappingList.ApplyAndClear( *this );
ExportNonMappingTasks();
// Adjust worktime to represent the slowest thread, since that's when all threads were finished.
// This makes it easier to see how well the actual thread processing is parallelized.
double Adjustment = (GStatistics.WorkTimeEnd - GStatistics.WorkTimeStart) - MaxThreadTime;
if ( Adjustment > 0.0 )
{
GStatistics.WorkTimeEnd -= Adjustment;
}
GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Finished, -1 ) );
// Let's say the main thread used up the whole parallel time.
GStatistics.ThreadStatistics.TotalTime += MaxThreadTime;
const float FinishAndExportTime = FPlatformTime::Seconds() - GStatistics.WorkTimeEnd;
DumpStats(Stats.SceneSetupTime + Stats.MainThreadLightingTime + FinishAndExportTime);
AggregateMesh->DumpCheckStats();
}
/** Exports tasks that are not mappings, if they are ready. */
void FStaticLightingSystem::ExportNonMappingTasks()
{
// Export volume lighting samples to Swarm if they are complete
if (bShouldExportVolumeSampleData)
{
bShouldExportVolumeSampleData = false;
Exporter.ExportVolumeLightingSamples(
DynamicObjectSettings.bVisualizeVolumeLightSamples,
VolumeLightingDebugOutput,
VolumeBounds.Origin,
VolumeBounds.BoxExtent,
VolumeLightingSamples);
// Release volume lighting samples unless they are being used by the lighting threads for shading
if (!DynamicObjectSettings.bVisualizeVolumeLightInterpolation)
{
VolumeLightingSamples.Empty();
}
// Tell Swarm the task is complete (if we're not in debugging mode).
if ( !IsDebugMode() )
{
FLightmassSwarm* Swarm = GetExporter().GetSwarm();
Swarm->TaskCompleted( PrecomputedVolumeLightingGuid );
}
}
CompleteVisibilityTaskList.ApplyAndClear(*this);
CompleteVolumetricLightmapTaskList.ApplyAndClear(*this);
{
TMap<const FLight*, FStaticShadowDepthMap*> CompletedStaticShadowDepthMapsCopy;
{
// Enter a critical section before modifying DominantSpotLightShadowInfos since the worker threads may also modify it at any time
FScopeLock Lock(&CompletedStaticShadowDepthMapsSync);
CompletedStaticShadowDepthMapsCopy = CompletedStaticShadowDepthMaps;
CompletedStaticShadowDepthMaps.Empty();
}
for (TMap<const FLight*,FStaticShadowDepthMap*>::TIterator It(CompletedStaticShadowDepthMapsCopy); It; ++It)
{
const FLight* Light = It.Key();
Exporter.ExportStaticShadowDepthMap(Light->Guid, *It.Value());
// Tell Swarm the task is complete (if we're not in debugging mode).
if (!IsDebugMode())
{
FLightmassSwarm* Swarm = GetExporter().GetSwarm();
Swarm->TaskCompleted(Light->Guid);
}
delete It.Value();
}
}
if (bShouldExportMeshAreaLightData)
{
Exporter.ExportMeshAreaLightData(MeshAreaLights, MeshAreaLightSettings.MeshAreaLightGeneratedDynamicLightSurfaceOffset);
// Tell Swarm the task is complete (if we're not in debugging mode).
if ( !IsDebugMode() )
{
FLightmassSwarm* Swarm = GetExporter().GetSwarm();
Swarm->TaskCompleted( MeshAreaLightDataGuid );
}
bShouldExportMeshAreaLightData = 0;
}
if (bShouldExportVolumeDistanceField)
{
Exporter.ExportVolumeDistanceField(VolumeSizeX, VolumeSizeY, VolumeSizeZ, VolumeDistanceFieldSettings.VolumeMaxDistance, DistanceFieldVolumeBounds, VolumeDistanceField);
// Tell Swarm the task is complete (if we're not in debugging mode).
if ( !IsDebugMode() )
{
FLightmassSwarm* Swarm = GetExporter().GetSwarm();
Swarm->TaskCompleted( VolumeDistanceFieldGuid );
}
bShouldExportVolumeDistanceField = 0;
}
}
int32 FStaticLightingSystem::GetNumShadowRays(int32 BounceNumber, bool bPenumbra) const
{
int32 NumShadowRaysResult = 0;
if (BounceNumber == 0 && bPenumbra)
{
NumShadowRaysResult = ShadowSettings.NumPenumbraShadowRays;
}
else if (BounceNumber == 0 && !bPenumbra)
{
NumShadowRaysResult = ShadowSettings.NumShadowRays;
}
else if (BounceNumber > 0)
{
// Use less rays for each progressive bounce, since the variance will matter less.
NumShadowRaysResult = FMath::Max(ShadowSettings.NumBounceShadowRays / BounceNumber, 1);
}
return NumShadowRaysResult;
}
int32 FStaticLightingSystem::GetNumUniformHemisphereSamples(int32 BounceNumber) const
{
int32 NumSamplesResult = CachedHemisphereSamples.Num();
checkSlow(BounceNumber > 0);
return NumSamplesResult;
}
int32 FStaticLightingSystem::GetNumPhotonImportanceHemisphereSamples() const
{
return PhotonMappingSettings.bUsePhotonMapping ?
FMath::TruncToInt(ImportanceTracingSettings.NumHemisphereSamples * PhotonMappingSettings.FinalGatherImportanceSampleFraction) : 0;
}
FBoxSphereBounds3f FStaticLightingSystem::GetImportanceBounds(bool bClampToScene) const
{
FBoxSphereBounds3f ImportanceBounds = Scene.GetImportanceBounds();
if (bClampToScene)
{
const FBoxSphereBounds3f SceneBounds = FBoxSphereBounds3f(AggregateMesh->GetBounds());
const float SceneToImportanceOriginSquared = (ImportanceBounds.Origin - SceneBounds.Origin).SizeSquared();
if (SceneToImportanceOriginSquared > FMath::Square(SceneBounds.SphereRadius))
{
// Disable the importance bounds if the center of the importance volume is outside of the scene.
ImportanceBounds.SphereRadius = 0.0f;
}
else if (SceneToImportanceOriginSquared > FMath::Square(SceneBounds.SphereRadius - ImportanceBounds.SphereRadius))
{
// Clamp the importance volume's radius so that all parts of it are inside the scene.
ImportanceBounds.SphereRadius = SceneBounds.SphereRadius - FMath::Sqrt(SceneToImportanceOriginSquared);
}
else if (SceneBounds.SphereRadius <= ImportanceBounds.SphereRadius)
{
// Disable the importance volume if it is larger than the scene.
ImportanceBounds.SphereRadius = 0.0f;
}
}
return ImportanceBounds;
}
/** Returns true if the specified position is inside any of the importance volumes. */
bool FStaticLightingSystem::IsPointInImportanceVolume(const FVector4f& Position, float Tolerance) const
{
if (Scene.ImportanceVolumes.Num() > 0)
{
return Scene.IsPointInImportanceVolume(Position, Tolerance);
}
else
{
return true;
}
}
/** Changes the scene's settings if necessary so that only valid combinations are used */
void FStaticLightingSystem::ValidateSettings(FScene& InScene)
{
//@todo - verify valid ranges of all settings
InScene.GeneralSettings.NumIndirectLightingBounces = FMath::Max(InScene.GeneralSettings.NumIndirectLightingBounces, 0);
InScene.GeneralSettings.IndirectLightingSmoothness = FMath::Clamp(InScene.GeneralSettings.IndirectLightingSmoothness, .25f, 10.0f);
InScene.GeneralSettings.IndirectLightingQuality = FMath::Clamp(InScene.GeneralSettings.IndirectLightingQuality, .1f, 100.0f);
InScene.GeneralSettings.ViewSingleBounceNumber = FMath::Min(InScene.GeneralSettings.ViewSingleBounceNumber, InScene.GeneralSettings.NumIndirectLightingBounces);
if (FMath::IsNearlyEqual(InScene.PhotonMappingSettings.IndirectPhotonDensity, 0.0f))
{
// Allocate all samples toward uniform sampling if there are no indirect photons
InScene.PhotonMappingSettings.FinalGatherImportanceSampleFraction = 0;
}
#if LIGHTMASS_DO_PROCESSING
if (!InScene.PhotonMappingSettings.bUseIrradiancePhotons)
#endif
{
InScene.PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces = false;
}
InScene.PhotonMappingSettings.FinalGatherImportanceSampleFraction = FMath::Clamp(InScene.PhotonMappingSettings.FinalGatherImportanceSampleFraction, 0.0f, 1.0f);
if (InScene.ImportanceTracingSettings.NumHemisphereSamples * (1.0f - InScene.PhotonMappingSettings.FinalGatherImportanceSampleFraction) < 1)
{
// Irradiance caching needs some uniform samples
InScene.IrradianceCachingSettings.bAllowIrradianceCaching = false;
}
if (InScene.PhotonMappingSettings.bUsePhotonMapping && !InScene.PhotonMappingSettings.bUseFinalGathering)
{
// Irradiance caching currently only supported with final gathering
InScene.IrradianceCachingSettings.bAllowIrradianceCaching = false;
}
InScene.PhotonMappingSettings.ConeFilterConstant = FMath::Max(InScene.PhotonMappingSettings.ConeFilterConstant, 1.0f);
if (!InScene.IrradianceCachingSettings.bAllowIrradianceCaching)
{
InScene.IrradianceCachingSettings.bUseIrradianceGradients = false;
}
if (InScene.IrradianceCachingSettings.bUseIrradianceGradients)
{
// Irradiance gradients require stratified sampling because the information from each sampled cell is used to calculate the gradient
InScene.ImportanceTracingSettings.bUseStratifiedSampling = true;
}
else
{
InScene.IrradianceCachingSettings.bShowGradientsOnly = false;
}
if (InScene.DynamicObjectSettings.bVisualizeVolumeLightInterpolation)
{
// Disable irradiance caching if we are visualizing volume light interpolation, otherwise we will be getting a twice interpolated result.
InScene.IrradianceCachingSettings.bAllowIrradianceCaching = false;
}
// Round up to nearest odd number
ShadowSettings.MinDistanceFieldUpsampleFactor = FMath::Clamp(ShadowSettings.MinDistanceFieldUpsampleFactor - ShadowSettings.MinDistanceFieldUpsampleFactor % 2 + 1, 1, 17);
ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceX = FMath::Max(ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceX, DELTA);
ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceY = FMath::Max(ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceY, DELTA);
InScene.IrradianceCachingSettings.InterpolationMaxAngle = FMath::Clamp(InScene.IrradianceCachingSettings.InterpolationMaxAngle, 0.0f, 90.0f);
InScene.IrradianceCachingSettings.PointBehindRecordMaxAngle = FMath::Clamp(InScene.IrradianceCachingSettings.PointBehindRecordMaxAngle, 0.0f, 90.0f);
InScene.IrradianceCachingSettings.DistanceSmoothFactor = FMath::Max(InScene.IrradianceCachingSettings.DistanceSmoothFactor, 1.0f);
InScene.IrradianceCachingSettings.AngleSmoothFactor = FMath::Max(InScene.IrradianceCachingSettings.AngleSmoothFactor, 1.0f);
InScene.IrradianceCachingSettings.SkyOcclusionSmoothnessReduction = FMath::Clamp(InScene.IrradianceCachingSettings.SkyOcclusionSmoothnessReduction, 0.1f, 1.0f);
if (InScene.GeneralSettings.IndirectLightingQuality > 50)
{
InScene.ImportanceTracingSettings.NumAdaptiveRefinementLevels += 2;
}
else if (InScene.GeneralSettings.IndirectLightingQuality > 10)
{
InScene.ImportanceTracingSettings.NumAdaptiveRefinementLevels += 1;
}
InScene.ShadowSettings.NumShadowRays = FMath::TruncToInt(InScene.ShadowSettings.NumShadowRays * FMath::Sqrt(InScene.GeneralSettings.IndirectLightingQuality));
InScene.ShadowSettings.NumPenumbraShadowRays = FMath::TruncToInt(InScene.ShadowSettings.NumPenumbraShadowRays * FMath::Sqrt(InScene.GeneralSettings.IndirectLightingQuality));
InScene.ImportanceTracingSettings.NumAdaptiveRefinementLevels = FMath::Min(InScene.ImportanceTracingSettings.NumAdaptiveRefinementLevels, MaxNumRefiningDepths);
}
/** Logs solver stats */
void FStaticLightingSystem::DumpStats(float TotalStaticLightingTime) const
{
FString SolverStats = TEXT("\n\n");
SolverStats += FString::Printf(TEXT("Total Static Lighting time: %7.2f seconds, %i threads\n"), TotalStaticLightingTime, NumStaticLightingThreads );
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Scene setup\n"), 100.0f * Stats.SceneSetupTime / TotalStaticLightingTime, Stats.SceneSetupTime);
if (Stats.NumMeshAreaLights > 0)
{
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Mesh Area Light setup\n"), 100.0f * Stats.MeshAreaLightSetupTime / TotalStaticLightingTime, Stats.MeshAreaLightSetupTime);
}
if (PhotonMappingSettings.bUsePhotonMapping)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Emit Direct Photons\n"), 100.0f * Stats.EmitDirectPhotonsTime / TotalStaticLightingTime, Stats.EmitDirectPhotonsTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Cache Indirect Photon Paths\n"), 100.0f * Stats.CachingIndirectPhotonPathsTime / TotalStaticLightingTime, Stats.CachingIndirectPhotonPathsTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Emit Indirect Photons\n"), 100.0f * Stats.EmitIndirectPhotonsTime / TotalStaticLightingTime, Stats.EmitIndirectPhotonsTime);
if (PhotonMappingSettings.bUseIrradiancePhotons)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Mark %.3f million Irradiance Photons\n"), 100.0f * Stats.IrradiancePhotonMarkingTime / TotalStaticLightingTime, Stats.IrradiancePhotonMarkingTime, Stats.NumIrradiancePhotons / 1000000.0f);
if (PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Cache %.3f million Irradiance Photon Samples on surfaces\n"), 100.0f * Stats.CacheIrradiancePhotonsTime / TotalStaticLightingTime, Stats.CacheIrradiancePhotonsTime, Stats.NumCachedIrradianceSamples / 1000000.0f);
}
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Calculate %.3f million Irradiance Photons\n"), 100.0f * Stats.IrradiancePhotonCalculatingTime / TotalStaticLightingTime, Stats.IrradiancePhotonCalculatingTime, Stats.NumFoundIrradiancePhotons / 1000000.0f);
}
}
if (Stats.PrecomputedVisibilitySetupTime / TotalStaticLightingTime > .02f)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs sPVS setup\n"), 100.0f * Stats.PrecomputedVisibilitySetupTime / TotalStaticLightingTime, Stats.PrecomputedVisibilitySetupTime);
}
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Lighting\n"), 100.0f * Stats.MainThreadLightingTime / TotalStaticLightingTime, Stats.MainThreadLightingTime);
const float UnaccountedMainThreadTime = FMath::Max(TotalStaticLightingTime - (Stats.SceneSetupTime + Stats.EmitDirectPhotonsTime + Stats.CachingIndirectPhotonPathsTime + Stats.EmitIndirectPhotonsTime + Stats.IrradiancePhotonMarkingTime + Stats.CacheIrradiancePhotonsTime + Stats.IrradiancePhotonCalculatingTime + Stats.MainThreadLightingTime), 0.0f);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Unaccounted\n"), 100.0f * UnaccountedMainThreadTime / TotalStaticLightingTime, UnaccountedMainThreadTime);
// Send the message in multiple parts since it cuts off in the middle otherwise
LogSolverMessage(SolverStats);
SolverStats = TEXT("");
if (PhotonMappingSettings.bUsePhotonMapping)
{
if (Stats.EmitDirectPhotonsTime / TotalStaticLightingTime > .02)
{
SolverStats += FString::Printf( TEXT("Total Direct Photon Emitting thread seconds: %.1f\n"), Stats.EmitDirectPhotonsThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Sampling Lights\n"), 100.0f * Stats.DirectPhotonsLightSamplingThreadTime / Stats.EmitDirectPhotonsThreadTime, Stats.DirectPhotonsLightSamplingThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Custom attenuation\n"), 100.0f * Stats.DirectCustomAttenuationThreadTime / Stats.EmitDirectPhotonsThreadTime, Stats.DirectCustomAttenuationThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Tracing\n"), 100.0f * Stats.DirectPhotonsTracingThreadTime / Stats.EmitDirectPhotonsThreadTime, Stats.DirectPhotonsTracingThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Processing results\n"), 100.0f * Stats.ProcessDirectPhotonsThreadTime / Stats.EmitDirectPhotonsThreadTime, Stats.ProcessDirectPhotonsThreadTime);
const float UnaccountedDirectPhotonThreadTime = FMath::Max(Stats.EmitDirectPhotonsThreadTime - (Stats.ProcessDirectPhotonsThreadTime + Stats.DirectPhotonsLightSamplingThreadTime + Stats.DirectPhotonsTracingThreadTime + Stats.DirectCustomAttenuationThreadTime), 0.0f);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Unaccounted\n"), 100.0f * UnaccountedDirectPhotonThreadTime / Stats.EmitDirectPhotonsThreadTime, UnaccountedDirectPhotonThreadTime);
}
if (Stats.EmitIndirectPhotonsTime / TotalStaticLightingTime > .02)
{
SolverStats += FString::Printf( TEXT("\n") );
SolverStats += FString::Printf( TEXT("Total Indirect Photon Emitting thread seconds: %.1f\n"), Stats.EmitIndirectPhotonsThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Sampling Lights\n"), 100.0f * Stats.LightSamplingThreadTime / Stats.EmitIndirectPhotonsThreadTime, Stats.LightSamplingThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Intersect Light rays\n"), 100.0f * Stats.IntersectLightRayThreadTime / Stats.EmitIndirectPhotonsThreadTime, Stats.IntersectLightRayThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs PhotonBounceTracing\n"), 100.0f * Stats.PhotonBounceTracingThreadTime / Stats.EmitIndirectPhotonsThreadTime, Stats.PhotonBounceTracingThreadTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Custom attenuation\n"), 100.0f * Stats.IndirectCustomAttenuationThreadTime / Stats.EmitIndirectPhotonsThreadTime, Stats.IndirectCustomAttenuationThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Processing results\n"), 100.0f * Stats.ProcessIndirectPhotonsThreadTime / Stats.EmitIndirectPhotonsThreadTime, Stats.ProcessIndirectPhotonsThreadTime);
const float UnaccountedIndirectPhotonThreadTime = FMath::Max(Stats.EmitIndirectPhotonsThreadTime - (Stats.ProcessIndirectPhotonsThreadTime + Stats.LightSamplingThreadTime + Stats.IntersectLightRayThreadTime + Stats.PhotonBounceTracingThreadTime), 0.0f);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Unaccounted\n"), 100.0f * UnaccountedIndirectPhotonThreadTime / Stats.EmitIndirectPhotonsThreadTime, UnaccountedIndirectPhotonThreadTime);
}
if (PhotonMappingSettings.bUseIrradiancePhotons)
{
if (PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces
// Only log Irradiance photon caching stats if it was more than 2 percent of the total time
&& Stats.CacheIrradiancePhotonsTime / TotalStaticLightingTime > .02)
{
SolverStats += FString::Printf( TEXT("\n") );
SolverStats += FString::Printf( TEXT("Total Irradiance Photon Caching thread seconds: %.1f\n"), Stats.IrradiancePhotonCachingThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Octree traversal\n"), 100.0f * Stats.IrradiancePhotonOctreeTraversalTime / Stats.IrradiancePhotonCachingThreadTime, Stats.IrradiancePhotonOctreeTraversalTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs %.3f million Visibility rays\n"), 100.0f * Stats.IrradiancePhotonSearchRayTime / Stats.IrradiancePhotonCachingThreadTime, Stats.IrradiancePhotonSearchRayTime, Stats.NumIrradiancePhotonSearchRays / 1000000.0f);
const float UnaccountedIrradiancePhotonCachingThreadTime = FMath::Max(Stats.IrradiancePhotonCachingThreadTime - (Stats.IrradiancePhotonOctreeTraversalTime + Stats.IrradiancePhotonSearchRayTime), 0.0f);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Unaccounted\n"), 100.0f * UnaccountedIrradiancePhotonCachingThreadTime / Stats.IrradiancePhotonCachingThreadTime, UnaccountedIrradiancePhotonCachingThreadTime);
}
// Only log Irradiance photon calculating stats if it was more than 2 percent of the total time
if (Stats.IrradiancePhotonCalculatingTime / TotalStaticLightingTime > .02)
{
SolverStats += FString::Printf( TEXT("\n") );
SolverStats += FString::Printf( TEXT("Total Calculating Irradiance Photons thread seconds: %.1f\n"), Stats.IrradiancePhotonCalculatingThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Pushing Octree Children\n"), 100.0f * Stats.CalculateIrradiancePhotonStats.PushingOctreeChildrenThreadTime / Stats.IrradiancePhotonCalculatingThreadTime, Stats.CalculateIrradiancePhotonStats.PushingOctreeChildrenThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Processing Octree Elements\n"), 100.0f * Stats.CalculateIrradiancePhotonStats.ProcessingOctreeElementsThreadTime / Stats.IrradiancePhotonCalculatingThreadTime, Stats.CalculateIrradiancePhotonStats.ProcessingOctreeElementsThreadTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Finding furthest photon\n"), 100.0f * Stats.CalculateIrradiancePhotonStats.FindingFurthestPhotonThreadTime / Stats.IrradiancePhotonCalculatingThreadTime, Stats.CalculateIrradiancePhotonStats.FindingFurthestPhotonThreadTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Calculating Irradiance\n"), 100.0f * Stats.CalculateIrradiancePhotonStats.CalculateIrradianceThreadTime / Stats.IrradiancePhotonCalculatingThreadTime, Stats.CalculateIrradiancePhotonStats.CalculateIrradianceThreadTime);
const float UnaccountedCalculateIrradiancePhotonsTime = FMath::Max(Stats.IrradiancePhotonCalculatingThreadTime -
(Stats.CalculateIrradiancePhotonStats.PushingOctreeChildrenThreadTime + Stats.CalculateIrradiancePhotonStats.ProcessingOctreeElementsThreadTime + Stats.CalculateIrradiancePhotonStats.CalculateIrradianceThreadTime), 0.0f);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Unaccounted\n"), 100.0f * UnaccountedCalculateIrradiancePhotonsTime / Stats.IrradiancePhotonCalculatingThreadTime, UnaccountedCalculateIrradiancePhotonsTime);
}
}
SolverStats += FString::Printf( TEXT("\n") );
SolverStats += FString::Printf( TEXT("Radiosity Setup thread seconds: %.1f, Radiosity Iteration thread seconds: %.1f\n"), Stats.RadiositySetupThreadTime, Stats.RadiosityIterationThreadTime);
}
// Send the message in multiple parts since it cuts off in the middle otherwise
LogSolverMessage(SolverStats);
SolverStats = TEXT("");
const float TotalLightingBusyThreadTime = Stats.TotalLightingThreadTime;
SolverStats += FString::Printf( TEXT("\n") );
SolverStats += FString::Printf( TEXT("Total busy Lighting thread seconds: %.2f\n"), TotalLightingBusyThreadTime);
const float SampleSetupTime = Stats.VertexSampleCreationTime + Stats.TexelRasterizationTime;
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Texel and vertex setup\n"), 100.0f * SampleSetupTime / TotalLightingBusyThreadTime, SampleSetupTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Direct lighting\n"), 100.0f * Stats.DirectLightingTime / TotalLightingBusyThreadTime, Stats.DirectLightingTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Area shadows with %.3f million rays\n"), 100.0f * Stats.AreaShadowsThreadTime / TotalLightingBusyThreadTime, Stats.AreaShadowsThreadTime, Stats.NumDirectLightingShadowRays / 1000000.0f);
if (Stats.AreaLightingThreadTime / TotalLightingBusyThreadTime > .04f)
{
SolverStats += FString::Printf( TEXT("%12.1f%%%8.1fs Area lighting\n"), 100.0f * Stats.AreaLightingThreadTime / TotalLightingBusyThreadTime, Stats.AreaLightingThreadTime);
}
if (Stats.NumSignedDistanceFieldCalculations > 0)
{
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Signed distance field source sparse sampling\n"), 100.0f * Stats.SignedDistanceFieldSourceFirstPassThreadTime / TotalLightingBusyThreadTime, Stats.SignedDistanceFieldSourceFirstPassThreadTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Signed distance field source refining sampling\n"), 100.0f * Stats.SignedDistanceFieldSourceSecondPassThreadTime / TotalLightingBusyThreadTime, Stats.SignedDistanceFieldSourceSecondPassThreadTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Signed distance field transition searching\n"), 100.0f * Stats.SignedDistanceFieldSearchThreadTime / TotalLightingBusyThreadTime, Stats.SignedDistanceFieldSearchThreadTime);
}
const float UnaccountedDirectLightingTime = FMath::Max(Stats.DirectLightingTime - (Stats.AreaShadowsThreadTime + Stats.SignedDistanceFieldSourceFirstPassThreadTime + Stats.SignedDistanceFieldSourceSecondPassThreadTime + Stats.SignedDistanceFieldSearchThreadTime), 0.0f);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Unaccounted\n"), 100.0f * UnaccountedDirectLightingTime / TotalLightingBusyThreadTime, UnaccountedDirectLightingTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Block on indirect lighting cache tasks\n"), 100.0f * Stats.BlockOnIndirectLightingCacheTasksTime / TotalLightingBusyThreadTime, Stats.BlockOnIndirectLightingCacheTasksTime);
if (IrradianceCachingSettings.bAllowIrradianceCaching)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Block on IrradianceCache Interpolation tasks\n"), 100.0f * Stats.BlockOnIndirectLightingInterpolateTasksTime / TotalLightingBusyThreadTime, Stats.BlockOnIndirectLightingInterpolateTasksTime);
}
if (Stats.StaticShadowDepthMapThreadTime > 0.1f)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Static shadow depth maps (max %.1fs)\n"), 100.0f * Stats.StaticShadowDepthMapThreadTime / TotalLightingBusyThreadTime, Stats.StaticShadowDepthMapThreadTime, Stats.MaxStaticShadowDepthMapThreadTime);
}
if (Stats.VolumeDistanceFieldThreadTime > 0.1f)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Volume distance field\n"), 100.0f * Stats.VolumeDistanceFieldThreadTime / TotalLightingBusyThreadTime, Stats.VolumeDistanceFieldThreadTime);
}
const float PrecomputedVisibilityThreadTime = Stats.PrecomputedVisibilityThreadTime;
if (PrecomputedVisibilityThreadTime > 0.1f)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Precomputed Visibility\n"), 100.0f * PrecomputedVisibilityThreadTime / TotalLightingBusyThreadTime, PrecomputedVisibilityThreadTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Sample generation\n"), 100.0f * Stats.PrecomputedVisibilitySampleSetupThreadTime / TotalLightingBusyThreadTime, Stats.PrecomputedVisibilitySampleSetupThreadTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Uniform tracing\n"), 100.0f * Stats.PrecomputedVisibilityRayTraceThreadTime / TotalLightingBusyThreadTime, Stats.PrecomputedVisibilityRayTraceThreadTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs Importance sampling\n"), 100.0f * Stats.PrecomputedVisibilityImportanceSampleThreadTime / TotalLightingBusyThreadTime, Stats.PrecomputedVisibilityImportanceSampleThreadTime);
}
if (Stats.NumDynamicObjectSurfaceSamples + Stats.NumDynamicObjectVolumeSamples > 0)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Volume Sample placement\n"), 100.0f * Stats.VolumeSamplePlacementThreadTime / TotalLightingBusyThreadTime, Stats.VolumeSamplePlacementThreadTime);
}
if (Stats.NumVolumetricLightmapSamples > 0)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Volumetric Lightmap - %.3f million samples\n"), 100.0f * Stats.TotalVolumetricLightmapLightingThreadTime / TotalLightingBusyThreadTime, Stats.TotalVolumetricLightmapLightingThreadTime, Stats.NumVolumetricLightmapSamples / 1000000.0f);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs VoxelizationTime\n"), 100.0f * Stats.VolumetricLightmapVoxelizationTime / TotalLightingBusyThreadTime, Stats.VolumetricLightmapVoxelizationTime);
if (Stats.VolumetricLightmapGatherImportancePhotonsTime > 0)
{
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs GatherImportancePhotons\n"), 100.0f * Stats.VolumetricLightmapGatherImportancePhotonsTime / TotalLightingBusyThreadTime, Stats.VolumetricLightmapGatherImportancePhotonsTime);
}
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs DirectLightingTime\n"), 100.0f * Stats.VolumetricLightmapDirectLightingTime / TotalLightingBusyThreadTime, Stats.VolumetricLightmapDirectLightingTime);
SolverStats += FString::Printf( TEXT("%8.1f%%%8.1fs FinalGatherTime\n"), 100.0f * Stats.VolumetricLightmapFinalGatherTime / TotalLightingBusyThreadTime, Stats.VolumetricLightmapFinalGatherTime);
}
const float UnaccountedLightingThreadTime = FMath::Max(TotalLightingBusyThreadTime - (SampleSetupTime + Stats.DirectLightingTime + Stats.BlockOnIndirectLightingCacheTasksTime + Stats.BlockOnIndirectLightingInterpolateTasksTime + Stats.IndirectLightingCacheTaskThreadTimeSeparateTask + Stats.SecondPassIrradianceCacheInterpolationTime + Stats.SecondPassIrradianceCacheInterpolationTimeSeparateTask + Stats.VolumeSamplePlacementThreadTime + Stats.StaticShadowDepthMapThreadTime + Stats.VolumeDistanceFieldThreadTime + PrecomputedVisibilityThreadTime + Stats.TotalVolumetricLightmapLightingThreadTime), 0.0f);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs Unaccounted\n"), 100.0f * UnaccountedLightingThreadTime / TotalLightingBusyThreadTime, UnaccountedLightingThreadTime);
// Send the message in multiple parts since it cuts off in the middle otherwise
LogSolverMessage(SolverStats);
SolverStats = TEXT("\n");
float IndirectLightingCacheTaskThreadTime = Stats.IndirectLightingCacheTaskThreadTime + Stats.IndirectLightingCacheTaskThreadTimeSeparateTask;
SolverStats += FString::Printf( TEXT("Indirect lighting cache task thread seconds: %.2f\n"), IndirectLightingCacheTaskThreadTime);
// These inner loop timings rely on rdtsc to avoid the massive overhead of Query Performance Counter.
// rdtsc is not dependable with multi-threading (see FRDTSCCycleTimer comments and Intel documentation) but we use it anyway because it's the only option.
//@todo - rdtsc is also not dependable if the OS changes which processor the thread gets executed on.
// Use SetThreadAffinityMask to prevent this case.
if (PhotonMappingSettings.bUsePhotonMapping)
{
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs ImportancePhotonGatherTime\n"), 100.0f * Stats.ImportancePhotonGatherTime / IndirectLightingCacheTaskThreadTime, Stats.ImportancePhotonGatherTime);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs CalculateImportanceSampleTime\n"), 100.0f * Stats.CalculateImportanceSampleTime / IndirectLightingCacheTaskThreadTime, Stats.CalculateImportanceSampleTime);
}
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs FirstBounceRayTraceTime for %.3f million rays\n"), 100.0f * Stats.FirstBounceRayTraceTime / IndirectLightingCacheTaskThreadTime, Stats.FirstBounceRayTraceTime, Stats.NumFirstBounceRaysTraced / 1000000.0f);
SolverStats += FString::Printf( TEXT("%4.1f%%%8.1fs CalculateExitantRadiance\n"), 100.0f * Stats.CalculateExitantRadianceTime / IndirectLightingCacheTaskThreadTime, Stats.CalculateExitantRadianceTime);
SolverStats += FString::Printf( TEXT("\n") );
SolverStats += FString::Printf( TEXT("Traced %.3f million first hit visibility rays for a total of %.1f thread seconds (%.3f million per thread second)\n"), Stats.NumFirstHitRaysTraced / 1000000.0f, Stats.FirstHitRayTraceThreadTime, Stats.NumFirstHitRaysTraced / 1000000.0f / Stats.FirstHitRayTraceThreadTime);
SolverStats += FString::Printf( TEXT("Traced %.3f million boolean visibility rays for a total of %.1f thread seconds (%.3f million per thread second)\n"), Stats.NumBooleanRaysTraced / 1000000.0f, Stats.BooleanRayTraceThreadTime, Stats.NumBooleanRaysTraced / 1000000.0f / Stats.BooleanRayTraceThreadTime);
const FBoxSphereBounds3f SceneBounds = FBoxSphereBounds3f(AggregateMesh->GetBounds());
const FBoxSphereBounds3f ImportanceBounds = GetImportanceBounds();
SolverStats += FString::Printf( TEXT("Scene radius %.1f, Importance bounds radius %.1f\n"), SceneBounds.SphereRadius, ImportanceBounds.SphereRadius);
SolverStats += FString::Printf( TEXT("%u Mappings, %.3f million Texels, %.3f million mapped texels\n"), Stats.NumMappings, Stats.NumTexelsProcessed / 1000000.0f, Stats.NumMappedTexels / 1000000.0f);
// Send the message in multiple parts since it cuts off in the middle otherwise
LogSolverMessage(SolverStats);
SolverStats = TEXT("");
float TextureMappingThreadTime = Stats.TotalTextureMappingLightingThreadTime + Stats.SecondPassIrradianceCacheInterpolationTimeSeparateTask + Stats.IndirectLightingCacheTaskThreadTimeSeparateTask;
const float UnaccountedMappingThreadTimePct = 100.0f * FMath::Max(TotalLightingBusyThreadTime - (TextureMappingThreadTime + Stats.TotalVolumeSampleLightingThreadTime + Stats.TotalVolumetricLightmapLightingThreadTime + PrecomputedVisibilityThreadTime), 0.0f) / TotalLightingBusyThreadTime;
SolverStats += FString::Printf( TEXT("%.1f%% of Total Lighting thread seconds on Texture Mappings, %1.f%% on Volume Samples, %1.f%% on Volumetric Lightmap, %1.f%% on Visibility, %.1f%% Unaccounted\n"), 100.0f * TextureMappingThreadTime / TotalLightingBusyThreadTime, 100.0f * Stats.TotalVolumeSampleLightingThreadTime / TotalLightingBusyThreadTime, 100.0f * Stats.TotalVolumetricLightmapLightingThreadTime / TotalLightingBusyThreadTime, 100.0f * PrecomputedVisibilityThreadTime / TotalLightingBusyThreadTime, UnaccountedMappingThreadTimePct);
SolverStats += FString::Printf( TEXT("%u Lights total, %.1f Shadow rays per light sample on average\n"), Stats.NumLights, Stats.NumDirectLightingShadowRays / (float)(Stats.NumMappedTexels + Stats.NumVertexSamples));
if (Stats.NumMeshAreaLights > 0)
{
SolverStats += FString::Printf( TEXT("%u Emissive meshes, %u Mesh area lights, %lld simplified mesh area light primitives, %lld original primitives\n"), Stats.NumMeshAreaLightMeshes, Stats.NumMeshAreaLights, Stats.NumSimplifiedMeshAreaLightPrimitives, Stats.NumMeshAreaLightPrimitives);
}
if (Stats.NumSignedDistanceFieldCalculations > 0)
{
SolverStats += FString::Printf( TEXT("Signed distance field shadows: %.1f average upsample factor, %.3f million sparse source rays, %.3f million refining source rays, %.3f million transition search scatters\n"), Stats.AccumulatedSignedDistanceFieldUpsampleFactors / Stats.NumSignedDistanceFieldCalculations, Stats.NumSignedDistanceFieldAdaptiveSourceRaysFirstPass / 1000000.0f, Stats.NumSignedDistanceFieldAdaptiveSourceRaysSecondPass / 1000000.0f, Stats.NumSignedDistanceFieldScatters / 1000000.0f);
}
const int32 TotalVolumeLightingSamples = Stats.NumDynamicObjectSurfaceSamples + Stats.NumDynamicObjectVolumeSamples;
if (TotalVolumeLightingSamples > 0)
{
SolverStats += FString::Printf( TEXT("%u Volume lighting samples, %.1f%% placed on surfaces, %.1f%% placed in the volume, %.1f thread seconds\n"), TotalVolumeLightingSamples, 100.0f * Stats.NumDynamicObjectSurfaceSamples / (float)TotalVolumeLightingSamples, 100.0f * Stats.NumDynamicObjectVolumeSamples / (float)TotalVolumeLightingSamples, Stats.TotalVolumeSampleLightingThreadTime);
}
if (Stats.NumPrecomputedVisibilityQueries > 0)
{
SolverStats += FString::Printf( TEXT("Precomputed Visibility %u Cells (%.1f%% from camera tracks, %u processed on this agent), %u Meshes, %.3f million rays, %.1fKb data\n"), Stats.NumPrecomputedVisibilityCellsTotal, 100.0f * Stats.NumPrecomputedVisibilityCellsCamaraTracks / Stats.NumPrecomputedVisibilityCellsTotal, Stats.NumPrecomputedVisibilityCellsProcessed, Stats.NumPrecomputedVisibilityMeshes, Stats.NumPrecomputedVisibilityRayTraces / 1000000.0f, Stats.PrecomputedVisibilityDataBytes / 1024.0f);
const uint64 NumQueriesVisible = Stats.NumQueriesVisibleByDistanceRatio + Stats.NumQueriesVisibleExplicitSampling + Stats.NumQueriesVisibleImportanceSampling;
const uint64 TotalNumQueries = Stats.NumPrecomputedVisibilityQueries + Stats.NumPrecomputedVisibilityGroupQueries;
SolverStats += FString::Printf( TEXT(" %.3f million mesh queries, %.3f million group queries, %.1f%% visible, (%.1f%% trivially visible, %.1f%% explicit sampling, %.1f%% importance sampling)\n"), Stats.NumPrecomputedVisibilityQueries / 1000000.0f, Stats.NumPrecomputedVisibilityGroupQueries / 1000000.0f, 100.0f * NumQueriesVisible / TotalNumQueries, 100.0f * Stats.NumQueriesVisibleByDistanceRatio / NumQueriesVisible, 100.0f * Stats.NumQueriesVisibleExplicitSampling / NumQueriesVisible, 100.0f * Stats.NumQueriesVisibleImportanceSampling / NumQueriesVisible);
SolverStats += FString::Printf( TEXT(" %ux%ux%u group cells with %u occupied, %u meshes individually queried, %.3f million mesh queries skipped\n"), GroupVisibilityGridSizeXY, GroupVisibilityGridSizeXY, GroupVisibilityGridSizeZ, VisibilityGroups.Num(), Stats.NumPrecomputedVisibilityMeshesExcludedFromGroups, Stats.NumPrecomputedVisibilityMeshQueriesSkipped / 1000000.0f);
}
// Send the message in multiple parts since it cuts off in the middle otherwise
LogSolverMessage(SolverStats);
SolverStats = TEXT("");
if (PhotonMappingSettings.bUsePhotonMapping)
{
const float FirstPassEmittedPhotonEfficiency = 100.0f * FMath::Max(Stats.NumDirectPhotonsGathered, NumIndirectPhotonPaths) / Stats.NumFirstPassPhotonsEmitted;
SolverStats += FString::Printf( TEXT("%.3f million first pass Photons Emitted (out of %.3f million requested) to deposit %.3f million Direct Photons and %u Indirect Photon Paths, efficiency of %.2f%%\n"), Stats.NumFirstPassPhotonsEmitted / 1000000.0f, Stats.NumFirstPassPhotonsRequested / 1000000.0f, Stats.NumDirectPhotonsGathered / 1000000.0f, NumIndirectPhotonPaths, FirstPassEmittedPhotonEfficiency);
const float SecondPassEmittedPhotonEfficiency = 100.0f * Stats.NumIndirectPhotonsGathered / Stats.NumSecondPassPhotonsEmitted;
SolverStats += FString::Printf( TEXT("%.3f million second pass Photons Emitted (out of %.3f million requested) to deposit %.3f million Indirect Photons, efficiency of %.2f%%\n"), Stats.NumSecondPassPhotonsEmitted / 1000000.0f, Stats.NumSecondPassPhotonsRequested / 1000000.0f, Stats.NumIndirectPhotonsGathered / 1000000.0f, SecondPassEmittedPhotonEfficiency);
SolverStats += FString::Printf( TEXT("%.3f million Photon Gathers, %.3f million Irradiance Photon Gathers\n"), Stats.NumPhotonGathers / 1000000.0f, Stats.NumIrradiancePhotonMapSearches / 1000000.0f);
SolverStats += FString::Printf( TEXT("%.3f million Importance Photons found, %.3f million Importance Photon PDF calculations\n"), Stats.TotalFoundImportancePhotons / 1000000.0f, Stats.NumImportancePDFCalculations / 1000000.0f);
if (PhotonMappingSettings.bUseIrradiancePhotons && Stats.IrradiancePhotonCalculatingTime / TotalStaticLightingTime > .02)
{
SolverStats += FString::Printf( TEXT("%.3f million Irradiance Photons, %.1f%% Direct, %.1f%% Indirect, %.3f million actually found\n"), Stats.NumIrradiancePhotons / 1000000.0f, 100.0f * Stats.NumDirectIrradiancePhotons / Stats.NumIrradiancePhotons, 100.0f * (Stats.NumIrradiancePhotons - Stats.NumDirectIrradiancePhotons) / Stats.NumIrradiancePhotons, Stats.NumFoundIrradiancePhotons / 1000000.0f);
const float IterationsPerSearch = Stats.CalculateIrradiancePhotonStats.NumSearchIterations / (float)Stats.CalculateIrradiancePhotonStats.NumIterativePhotonMapSearches;
if (Stats.CalculateIrradiancePhotonStats.NumIterativePhotonMapSearches > 0)
{
SolverStats += FString::Printf( TEXT("%.1f Irradiance calculating search iterations per search (%.3f million searches, %.3f million iterations)\n"), IterationsPerSearch, Stats.CalculateIrradiancePhotonStats.NumIterativePhotonMapSearches / 1000000.0f, Stats.CalculateIrradiancePhotonStats.NumSearchIterations / 1000000.0f);
}
SolverStats += FString::Printf( TEXT("%.3f million octree nodes tested during irradiance photon calculating, %.3f million nodes visited, %.3f million elements tested, %.3f million elements accepted\n"), Stats.CalculateIrradiancePhotonStats.NumOctreeNodesTested / 1000000.0f, Stats.CalculateIrradiancePhotonStats.NumOctreeNodesVisited / 1000000.0f, Stats.CalculateIrradiancePhotonStats.NumElementsTested / 1000000.0f, Stats.CalculateIrradiancePhotonStats.NumElementsAccepted / 1000000.0f);
}
}
if (IrradianceCachingSettings.bAllowIrradianceCaching)
{
const int32 NumIrradianceCacheBounces = PhotonMappingSettings.bUsePhotonMapping ? 1 : GeneralSettings.NumIndirectLightingBounces;
for (int32 BounceIndex = 0; BounceIndex < NumIrradianceCacheBounces; BounceIndex++)
{
const FIrradianceCacheStats& CurrentStats = Stats.Cache[BounceIndex];
if (CurrentStats.NumCacheLookups > 0)
{
const float MissRate = 100.0f * CurrentStats.NumRecords / CurrentStats.NumCacheLookups;
SolverStats += FString::Printf( TEXT("%.1f%% Bounce %i Irradiance cache miss rate (%.3f million lookups, %.3f million misses, %.3f million inside geometry)\n"), MissRate, BounceIndex + 1, CurrentStats.NumCacheLookups / 1000000.0f, CurrentStats.NumRecords / 1000000.0f, CurrentStats.NumInsideGeometry / 1000000.0f);
}
}
}
if (PhotonMappingSettings.bUseFinalGathering)
{
uint64 TotalNumRefiningSamples = 0;
for (int i = 0; i < ImportanceTracingSettings.NumAdaptiveRefinementLevels; i++)
{
TotalNumRefiningSamples += Stats.NumRefiningFinalGatherSamples[i];
}
SolverStats += FString::Printf( TEXT("Final Gather: %.1fs on %.3f million base samples, %.1fs on %.3f million refining samples for %u refinement levels. \n"), Stats.BaseFinalGatherSampleTime, Stats.NumBaseFinalGatherSamples / 1000000.0f, Stats.RefiningFinalGatherSampleTime, TotalNumRefiningSamples / 1000000.0f, ImportanceTracingSettings.NumAdaptiveRefinementLevels);
if (TotalNumRefiningSamples > 0)
{
SolverStats += FString::Printf( TEXT(" %.1f%% due to brightness differences, %.1f%% due to importance photons, %.1f%% other reasons, Samples at depth: "), 100.0f * Stats.NumRefiningSamplesDueToBrightness / TotalNumRefiningSamples, 100.0f * Stats.NumRefiningSamplesDueToImportancePhotons / TotalNumRefiningSamples, 100.0f * Stats.NumRefiningSamplesOther / TotalNumRefiningSamples);
for (int i = 0; i < ImportanceTracingSettings.NumAdaptiveRefinementLevels; i++)
{
SolverStats += FString::Printf(TEXT("%.1f%%, "), 100.0f * Stats.NumRefiningFinalGatherSamples[i] / TotalNumRefiningSamples);
}
SolverStats += FString::Printf(TEXT("\n"));
}
}
#if PLATFORM_WINDOWS
PROCESS_MEMORY_COUNTERS_EX ProcessMemoryInfo;
if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&ProcessMemoryInfo, sizeof(ProcessMemoryInfo)))
{
SolverStats += FString::Printf( TEXT("%.1f Mb Peak Working Set\n"), ProcessMemoryInfo.PeakWorkingSetSize / (1024.0f * 1024.0f));
}
else
{
SolverStats += TEXT("GetProcessMemoryInfo Failed!");
}
SolverStats += FString::Printf( TEXT("\n") );
#elif PLATFORM_MAC
{
struct rusage MemUsage;
if (getrusage( RUSAGE_SELF, &MemUsage ) == 0)
{
SolverStats += FString::Printf( TEXT("%.1f Mb Peak Working Set\n"), MemUsage.ru_maxrss / (1024.0f * 1024.0f));
}
else
{
SolverStats += TEXT("getrusage() failed!");
}
}
SolverStats += FString::Printf( TEXT("\n") );
#endif
LogSolverMessage(SolverStats);
const bool bDumpMemoryStats = false;
if (bDumpMemoryStats)
{
#if PLATFORM_WINDOWS
PROCESS_MEMORY_COUNTERS ProcessMemory;
verify(GetProcessMemoryInfo(GetCurrentProcess(), &ProcessMemory, sizeof(ProcessMemory)));
UE_LOG(LogLightmass, Log, TEXT("Virtual memory used %.1fMb, Peak %.1fMb"),
ProcessMemory.PagefileUsage / 1048576.0f,
ProcessMemory.PeakPagefileUsage / 1048576.0f);
#elif PLATFORM_MAC
{
task_basic_info_64_data_t TaskInfo;
mach_msg_type_number_t TaskInfoCount = TASK_BASIC_INFO_COUNT;
task_info( mach_task_self(), TASK_BASIC_INFO, (task_info_t)&TaskInfo, &TaskInfoCount );
UE_LOG(LogLightmass, Log, TEXT("Virtual memory used %.1fMb"),
TaskInfo.virtual_size / 1048576.0f); // can't get peak virtual memory on Mac
}
#endif
AggregateMesh->DumpStats();
UE_LOG(LogLightmass, Log, TEXT("DirectPhotonMap"));
DirectPhotonMap.DumpStats(false);
UE_LOG(LogLightmass, Log, TEXT("FirstBouncePhotonMap"));
FirstBouncePhotonMap.DumpStats(false);
UE_LOG(LogLightmass, Log, TEXT("FirstBounceEscapedPhotonMap"));
FirstBounceEscapedPhotonMap.DumpStats(false);
UE_LOG(LogLightmass, Log, TEXT("FirstBouncePhotonSegmentMap"));
FirstBouncePhotonSegmentMap.DumpStats(false);
UE_LOG(LogLightmass, Log, TEXT("SecondBouncePhotonMap"));
SecondBouncePhotonMap.DumpStats(false);
UE_LOG(LogLightmass, Log, TEXT("IrradiancePhotonMap"));
IrradiancePhotonMap.DumpStats(false);
uint64 IrradiancePhotonCacheBytes = 0;
for (int32 i = 0; i < AllMappings.Num(); i++)
{
IrradiancePhotonCacheBytes += AllMappings[i]->GetIrradiancePhotonCacheBytes();
}
UE_LOG(LogLightmass, Log, TEXT("%.3fMb for Irradiance Photon surface caches"), IrradiancePhotonCacheBytes / 1048576.0f);
}
}
/** Logs a solver message */
void FStaticLightingSystem::LogSolverMessage(const FString& Message) const
{
if (Scene.DebugInput.bRelaySolverStats)
{
// Relay the message back to Unreal if allowed
GSwarm->SendMessage(NSwarm::FInfoMessage(*Message));
}
GLog->Log(*Message);
}
/** Logs a progress update message when appropriate */
void FStaticLightingSystem::UpdateInternalStatus(int32 OldNumTexelsCompleted) const
{
const int32 NumProgressSteps = 10;
const float InvTotal = 1.0f / Stats.NumTexelsProcessed;
const float PreviousCompletedFraction = OldNumTexelsCompleted * InvTotal;
const float CurrentCompletedFraction = NumTexelsCompleted * InvTotal;
// Only log NumProgressSteps times
if (FMath::TruncToInt(PreviousCompletedFraction * NumProgressSteps) < FMath::TruncToInt(CurrentCompletedFraction * NumProgressSteps))
{
LogSolverMessage(FString::Printf(TEXT("Lighting %.1f%%"), CurrentCompletedFraction * 100.0f));
}
}
/** Caches samples for any sampling distributions that are known ahead of time, which greatly reduces noise in those estimates in exchange for structured artifacts. */
void FStaticLightingSystem::CacheSamples()
{
FLMRandomStream RandomStream(102341);
int32 NumUniformHemisphereSamples = 0;
if (PhotonMappingSettings.bUsePhotonMapping)
{
const float NumSamplesFloat =
ImportanceTracingSettings.NumHemisphereSamples
* GeneralSettings.IndirectLightingQuality;
NumUniformHemisphereSamples = FMath::TruncToInt(NumSamplesFloat);
}
else
{
NumUniformHemisphereSamples = ImportanceTracingSettings.NumHemisphereSamples;
}
CachedHemisphereSamples.Empty(NumUniformHemisphereSamples);
CachedHemisphereSampleUniforms.Empty(NumUniformHemisphereSamples);
if (ImportanceTracingSettings.bUseStratifiedSampling)
{
// Split the sampling domain up into cells with equal area
// Using PI times more Phi steps as Theta steps, but the relationship between them could be anything
const float NumThetaStepsFloat = FMath::Sqrt(NumUniformHemisphereSamples / (float)PI);
const int32 NumThetaSteps = FMath::TruncToInt(NumThetaStepsFloat);
const int32 NumPhiSteps = FMath::TruncToInt(NumThetaStepsFloat * (float)PI);
GenerateStratifiedUniformHemisphereSamples(NumThetaSteps, NumPhiSteps, RandomStream, CachedHemisphereSamples, CachedHemisphereSampleUniforms);
}
else
{
for (int32 SampleIndex = 0; SampleIndex < NumUniformHemisphereSamples; SampleIndex++)
{
const FVector4f& CurrentSample = GetUniformHemisphereVector(RandomStream, ImportanceTracingSettings.MaxHemisphereRayAngle);
CachedHemisphereSamples.Add(CurrentSample);
}
}
{
FVector4f CombinedVector(0);
for (int32 SampleIndex = 0; SampleIndex < CachedHemisphereSamples.Num(); SampleIndex++)
{
CombinedVector += CachedHemisphereSamples[SampleIndex];
}
CachedSamplesMaxUnoccludedLength = (CombinedVector / CachedHemisphereSamples.Num()).Size3();
}
for (int32 SampleSet = 0; SampleSet < UE_ARRAY_COUNT(CachedHemisphereSamplesForRadiosity); SampleSet++)
{
float SampleSetScale = FMath::Lerp(.5f, .125f, SampleSet / ((float)UE_ARRAY_COUNT(CachedHemisphereSamplesForRadiosity) - 1));
int32 TargetNumApproximateSkyLightingSamples = FMath::Max(FMath::TruncToInt(ImportanceTracingSettings.NumHemisphereSamples * SampleSetScale * GeneralSettings.IndirectLightingQuality), 12);
CachedHemisphereSamplesForRadiosity[SampleSet].Empty(TargetNumApproximateSkyLightingSamples);
CachedHemisphereSamplesForRadiosityUniforms[SampleSet].Empty(TargetNumApproximateSkyLightingSamples);
const float NumThetaStepsFloat = FMath::Sqrt(TargetNumApproximateSkyLightingSamples / (float)PI);
const int32 NumThetaSteps = FMath::TruncToInt(NumThetaStepsFloat);
const int32 NumPhiSteps = FMath::TruncToInt(NumThetaStepsFloat * (float)PI);
GenerateStratifiedUniformHemisphereSamples(NumThetaSteps, NumPhiSteps, RandomStream, CachedHemisphereSamplesForRadiosity[SampleSet], CachedHemisphereSamplesForRadiosityUniforms[SampleSet]);
}
// Cache samples on the surface of each light for area shadows
for (int32 LightIndex = 0; LightIndex < Lights.Num(); LightIndex++)
{
FLight* Light = Lights[LightIndex];
for (int32 BounceIndex = 0; BounceIndex < GeneralSettings.NumIndirectLightingBounces + 1; BounceIndex++)
{
const int32 NumPenumbraTypes = BounceIndex == 0 ? 2 : 1;
Light->CacheSurfaceSamples(BounceIndex, GetNumShadowRays(BounceIndex, false), GetNumShadowRays(BounceIndex, true), RandomStream);
}
}
{
const int32 NumUpperVolumeSamples = ImportanceTracingSettings.NumHemisphereSamples * DynamicObjectSettings.NumHemisphereSamplesScale;
const float NumThetaStepsFloat = FMath::Sqrt(NumUpperVolumeSamples / (float)PI);
const int32 NumThetaSteps = FMath::TruncToInt(NumThetaStepsFloat);
const int32 NumPhiSteps = FMath::TruncToInt(NumThetaStepsFloat * (float)PI);
GenerateStratifiedUniformHemisphereSamples(NumThetaSteps, NumPhiSteps, RandomStream, CachedVolumetricLightmapUniformHemisphereSamples, CachedVolumetricLightmapUniformHemisphereSampleUniforms);
FVector4f CombinedVector(0);
for (int32 SampleIndex = 0; SampleIndex < CachedVolumetricLightmapUniformHemisphereSamples.Num(); SampleIndex++)
{
CombinedVector += CachedVolumetricLightmapUniformHemisphereSamples[SampleIndex];
}
CachedVolumetricLightmapMaxUnoccludedLength = (CombinedVector / CachedVolumetricLightmapUniformHemisphereSamples.Num()).Size3();
}
CachedVolumetricLightmapVertexOffsets.Add(FVector3f(0, 0, 0));
}
bool FStaticLightingThreadRunnable::CheckHealth(bool bReportError) const
{
if( bTerminatedByError && bReportError )
{
UE_LOG(LogLightmass, Fatal, TEXT("Static lighting thread exception:\r\n%s"), *ErrorMessage );
}
return !bTerminatedByError;
}
void FStaticLightingThreadRunnable::FixThreadGroupAffinity() const
{
#if PLATFORM_WINDOWS
int NumProcessorGroups = GetActiveProcessorGroupCount();
int PrefixSum = 0;
for (int GroupIndex = 0; GroupIndex < NumProcessorGroups; GroupIndex++)
{
int NumProcessorsThisGroup = GetActiveProcessorCount(GroupIndex);
if (ThreadIndex < PrefixSum + NumProcessorsThisGroup)
{
GROUP_AFFINITY GroupAffinity = {};
GroupAffinity.Group = GroupIndex;
GroupAffinity.Mask = (1ull << NumProcessorsThisGroup) - 1;
check(SetThreadGroupAffinity(GetCurrentThread(), &GroupAffinity, NULL));
break;
}
PrefixSum += NumProcessorsThisGroup;
}
#endif
}
uint32 FMappingProcessingThreadRunnable::Run()
{
const double StartThreadTime = FPlatformTime::Seconds();
FixThreadGroupAffinity();
#if PLATFORM_WINDOWS
if(!FPlatformMisc::IsDebuggerPresent())
{
__try
{
if (TaskType == StaticLightingTask_ProcessMappings)
{
System->ThreadLoop(false, ThreadIndex, ThreadStatistics, IdleTime);
}
else if (TaskType == StaticLightingTask_CacheIrradiancePhotons)
{
System->CacheIrradiancePhotonsThreadLoop(ThreadIndex, false);
}
else if (TaskType == StaticLightingTask_RadiositySetup)
{
System->RadiositySetupThreadLoop(ThreadIndex, false);
}
else if (TaskType == StaticLightingTask_RadiosityIterations)
{
System->RadiosityIterationThreadLoop(ThreadIndex, false);
}
else if (TaskType == StaticLightingTask_FinalizeSurfaceCache)
{
System->FinalizeSurfaceCacheThreadLoop(ThreadIndex, false);
}
else
{
UE_LOG(LogLightmass, Fatal, TEXT("Unsupported task type"));
}
}
__except( ReportCrash( GetExceptionInformation() ) )
{
ErrorMessage = GErrorHist;
bTerminatedByError = true;
}
}
else
#endif
{
if (TaskType == StaticLightingTask_ProcessMappings)
{
System->ThreadLoop(false, ThreadIndex, ThreadStatistics, IdleTime);
}
else if (TaskType == StaticLightingTask_CacheIrradiancePhotons)
{
System->CacheIrradiancePhotonsThreadLoop(ThreadIndex, false);
}
else if (TaskType == StaticLightingTask_RadiositySetup)
{
System->RadiositySetupThreadLoop(ThreadIndex, false);
}
else if (TaskType == StaticLightingTask_RadiosityIterations)
{
System->RadiosityIterationThreadLoop(ThreadIndex, false);
}
else if (TaskType == StaticLightingTask_FinalizeSurfaceCache)
{
System->FinalizeSurfaceCacheThreadLoop(ThreadIndex, false);
}
else
{
UE_LOG(LogLightmass, Fatal, TEXT("Unsupported task type"));
}
}
ExecutionTime = FPlatformTime::Seconds() - StartThreadTime;
FinishedCounter.Increment();
return 0;
}
/**
* Retrieves the next task from Swarm. Blocking, thread-safe function call. Returns NULL when there are no more tasks.
* @return The next mapping task to process.
*/
FStaticLightingMapping* FStaticLightingSystem::ThreadGetNextMapping(
FThreadStatistics& ThreadStatistics,
FGuid& TaskGuid,
uint32 WaitTime,
bool& bWaitTimedOut,
bool& bDynamicObjectTask,
int32& PrecomputedVisibilityTaskIndex,
int32& VolumetricLightmapTaskIndex,
bool& bStaticShadowDepthMapTask,
bool& bMeshAreaLightDataTask,
bool& bVolumeDataTask)
{
FStaticLightingMapping* Mapping = NULL;
// Initialize output parameters
bWaitTimedOut = true;
bDynamicObjectTask = false;
PrecomputedVisibilityTaskIndex = INDEX_NONE;
VolumetricLightmapTaskIndex = INDEX_NONE;
bStaticShadowDepthMapTask = false;
bMeshAreaLightDataTask = false;
bVolumeDataTask = false;
if ( GDebugMode )
{
FScopeLock Lock( &CriticalSection );
bWaitTimedOut = false;
// If we're in debugging mode, just grab the next mapping from the scene.
TMap<FGuid, FStaticLightingMapping*>::TIterator It(Mappings);
if ( It )
{
Mapping = It.Value();
It.RemoveCurrent();
}
}
else
{
// Request a new task from Swarm.
FLightmassSwarm* Swarm = Exporter.GetSwarm();
double SwarmRequestStart = FPlatformTime::Seconds();
bool bGotTask = Swarm->RequestTask( TaskGuid, WaitTime );
double SwarmRequestEnd = FPlatformTime::Seconds();
if ( bGotTask )
{
if (TaskGuid == PrecomputedVolumeLightingGuid)
{
bDynamicObjectTask = true;
Swarm->AcceptTask( TaskGuid );
bWaitTimedOut = false;
}
else if (TaskGuid == MeshAreaLightDataGuid)
{
bMeshAreaLightDataTask = true;
Swarm->AcceptTask( TaskGuid );
bWaitTimedOut = false;
}
else if (TaskGuid == VolumeDistanceFieldGuid)
{
bVolumeDataTask = true;
Swarm->AcceptTask( TaskGuid );
bWaitTimedOut = false;
}
else if (Scene.FindLightByGuid(TaskGuid))
{
bStaticShadowDepthMapTask = true;
Swarm->AcceptTask( TaskGuid );
bWaitTimedOut = false;
}
else
{
int32 FoundVisibilityIndex = INDEX_NONE;
Scene.VisibilityBucketGuids.Find(TaskGuid, FoundVisibilityIndex);
if (FoundVisibilityIndex >= 0)
{
PrecomputedVisibilityTaskIndex = FoundVisibilityIndex;
Swarm->AcceptTask(TaskGuid);
bWaitTimedOut = false;
}
else
{
int32 FoundVolumetricLightmapIndex = INDEX_NONE;
Scene.VolumetricLightmapTaskGuids.Find(TaskGuid, FoundVolumetricLightmapIndex);
if (FoundVolumetricLightmapIndex >= 0)
{
VolumetricLightmapTaskIndex = FoundVolumetricLightmapIndex;
Swarm->AcceptTask(TaskGuid);
bWaitTimedOut = false;
}
else
{
FStaticLightingMapping** MappingPtr = Mappings.Find(TaskGuid);
if (MappingPtr && FPlatformAtomics::InterlockedExchange(&(*MappingPtr)->bProcessed, true) == false)
{
// We received a new mapping to process. Tell Swarm we accept the task.
Swarm->AcceptTask(TaskGuid);
bWaitTimedOut = false;
Mapping = *MappingPtr;
}
else
{
// Couldn't find the mapping. Tell Swarm we reject the task and try again later.
UE_LOG(LogLightmass, Log, TEXT("Lightmass - Rejecting task (%08X%08X%08X%08X)!"), TaskGuid.A, TaskGuid.B, TaskGuid.C, TaskGuid.D);
Swarm->RejectTask(TaskGuid);
}
}
}
}
}
else if ( Swarm->ReceivedQuitRequest() || Swarm->IsDone() )
{
bWaitTimedOut = false;
}
ThreadStatistics.SwarmRequestTime += SwarmRequestEnd - SwarmRequestStart;
}
return Mapping;
}
void FStaticLightingSystem::ThreadLoop(bool bIsMainThread, int32 ThreadIndex, FThreadStatistics& ThreadStatistics, float& IdleTime)
{
const double ThreadTimeStart = FPlatformTime::Seconds();
GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_Processing0, ThreadIndex ) );
bool bSignaledMappingsComplete = false;
bool bIsDone = false;
while (!bIsDone)
{
const double StartLoopTime = FPlatformTime::Seconds();
bool bAnyTaskProcessedByThisThread = false;
// Process any existing local tasks before fetching new tasks from Swarm
{
while (FCacheIndirectTaskDescription * NextCacheTask = CacheIndirectLightingTasks.Pop())
{
//UE_LOG(LogLightmass, Warning, TEXT("Thread %u picked up Cache Indirect task for %u"), ThreadIndex, NextCacheTask->TextureMapping->Guid.D);
ProcessCacheIndirectLightingTask(NextCacheTask, false);
NextCacheTask->TextureMapping->CompletedCacheIndirectLightingTasks.Push(NextCacheTask);
FPlatformAtomics::InterlockedDecrement(&NextCacheTask->TextureMapping->NumOutstandingCacheTasks);
bAnyTaskProcessedByThisThread = true;
}
while (FInterpolateIndirectTaskDescription * NextInterpolateTask = InterpolateIndirectLightingTasks.Pop())
{
//UE_LOG(LogLightmass, Warning, TEXT("Thread %u picked up Interpolate indirect task for %u"), ThreadIndex, NextInterpolateTask->TextureMapping->Guid.D);
ProcessInterpolateTask(NextInterpolateTask, false);
NextInterpolateTask->TextureMapping->CompletedInterpolationTasks.Push(NextInterpolateTask);
FPlatformAtomics::InterlockedDecrement(&NextInterpolateTask->TextureMapping->NumOutstandingInterpolationTasks);
bAnyTaskProcessedByThisThread = true;
}
bAnyTaskProcessedByThisThread |= ProcessVolumetricLightmapTaskIfAvailable();
while (NumOutstandingVolumeDataLayers > 0)
{
const int32 ThreadZ = FPlatformAtomics::InterlockedIncrement(&OutstandingVolumeDataLayerIndex);
if (ThreadZ < VolumeSizeZ)
{
CalculateVolumeDistanceFieldWorkRange(ThreadZ);
const int32 NumTasksRemaining = FPlatformAtomics::InterlockedDecrement(&NumOutstandingVolumeDataLayers);
if (NumTasksRemaining == 0)
{
FPlatformAtomics::InterlockedExchange(&bShouldExportVolumeDistanceField, true);
}
bAnyTaskProcessedByThisThread = true;
}
}
while (NumVolumeSampleTasksOutstanding > 0)
{
const int32 TaskIndex = FPlatformAtomics::InterlockedIncrement(&NextVolumeSampleTaskIndex);
if (TaskIndex < VolumeSampleTasks.Num())
{
ProcessVolumeSamplesTask(VolumeSampleTasks[TaskIndex]);
const int32 NumTasksRemaining = FPlatformAtomics::InterlockedDecrement(&NumVolumeSampleTasksOutstanding);
if (NumTasksRemaining == 0)
{
FPlatformAtomics::InterlockedExchange(&bShouldExportVolumeSampleData, true);
}
bAnyTaskProcessedByThisThread = true;
}
}
}
uint32 DefaultRequestForTaskTimeout = 0;
FGuid TaskGuid;
bool bRequestForTaskTimedOut;
bool bDynamicObjectTask;
int32 PrecomputedVisibilityTaskIndex;
int32 VolumetricLightmapTaskIndex;
bool bStaticShadowDepthMapTask;
bool bMeshAreaLightDataTask;
bool bVolumeDataTask;
const double RequestTimeStart = FPlatformTime::Seconds();
FStaticLightingMapping* Mapping = ThreadGetNextMapping(
ThreadStatistics,
TaskGuid,
DefaultRequestForTaskTimeout,
bRequestForTaskTimedOut,
bDynamicObjectTask,
PrecomputedVisibilityTaskIndex,
VolumetricLightmapTaskIndex,
bStaticShadowDepthMapTask,
bMeshAreaLightDataTask,
bVolumeDataTask);
const double RequestTimeEnd = FPlatformTime::Seconds();
ThreadStatistics.RequestTime += RequestTimeEnd - RequestTimeStart;
if (Mapping)
{
const double MappingTimeStart = FPlatformTime::Seconds();
// Build the mapping's static lighting.
if(Mapping->GetTextureMapping())
{
check(!Mapping->GetVolumeMapping());
check(!Mapping->GetLandscapeVolumeMapping());
ProcessTextureMapping(Mapping->GetTextureMapping());
double MappingTimeEnd = FPlatformTime::Seconds();
ThreadStatistics.TextureMappingTime += MappingTimeEnd - MappingTimeStart;
ThreadStatistics.NumTextureMappings++;
}
}
else if (bDynamicObjectTask)
{
BeginCalculateVolumeSamples();
// If we didn't generate any samples then we can end the task
if (!IsDebugMode() && NumVolumeSampleTasksOutstanding <= 0)
{
FLightmassSwarm* Swarm = GetExporter().GetSwarm();
Swarm->TaskCompleted(PrecomputedVolumeLightingGuid);
}
}
else if (PrecomputedVisibilityTaskIndex >= 0)
{
CalculatePrecomputedVisibility(PrecomputedVisibilityTaskIndex);
}
else if (VolumetricLightmapTaskIndex >= 0)
{
CalculateAdaptiveVolumetricLightmap(VolumetricLightmapTaskIndex);
}
else if (bMeshAreaLightDataTask)
{
FPlatformAtomics::InterlockedExchange(&bShouldExportMeshAreaLightData, true);
}
else if (bVolumeDataTask)
{
BeginCalculateVolumeDistanceField();
}
else if (bStaticShadowDepthMapTask)
{
CalculateStaticShadowDepthMap(TaskGuid);
}
else
{
if (!bAnyTaskProcessedByThisThread && TasksInProgressThatWillNeedHelp <= 0 && NumOutstandingVolumeDataLayers <= 0 && NumVolumeSampleTasksOutstanding <= 0)
{
if (!bSignaledMappingsComplete)
{
bSignaledMappingsComplete = true;
GSwarm->SendMessage(NSwarm::FTimingMessage(NSwarm::PROGSTATE_Processing0, ThreadIndex));
}
if (!bRequestForTaskTimedOut)
{
// All mappings have been processed, so end this thread.
bIsDone = true;
}
else
{
FPlatformProcess::Sleep(.001f);
IdleTime += FPlatformTime::Seconds() - StartLoopTime;
}
}
}
// NOTE: Main thread shouldn't be running this anymore.
check( !bIsMainThread );
}
ThreadStatistics.TotalTime += FPlatformTime::Seconds() - ThreadTimeStart;
FPlatformAtomics::InterlockedIncrement(&GStatistics.NumThreadsFinished);
}
/**
* Applies the static lighting to the mappings in the list, and clears the list.
* Also reports back to Unreal after each mapping has been exported.
* @param LightingSystem - Reference to the static lighting system
*/
template<typename StaticLightingDataType>
void TCompleteStaticLightingList<StaticLightingDataType>::ApplyAndClear(FStaticLightingSystem& LightingSystem)
{
while(FirstElement)
{
// Atomically read the complete list and clear the shared head pointer.
TList<StaticLightingDataType>* LocalFirstElement;
TList<StaticLightingDataType>* CurrentElement;
uint32 ElementCount = 0;
do { LocalFirstElement = FirstElement; }
while(FPlatformAtomics::InterlockedCompareExchangePointer((void**)&FirstElement,NULL,LocalFirstElement) != LocalFirstElement);
// Should never be null, but this satisfies static-analysis
if (!LocalFirstElement)
continue;
// Traverse the local list, count the number of entries, and find the minimum guid
TList<StaticLightingDataType>* PreviousElement = NULL;
TList<StaticLightingDataType>* MinimumElementLink = NULL;
TList<StaticLightingDataType>* MinimumElement = NULL;
CurrentElement = LocalFirstElement;
MinimumElement = CurrentElement;
FGuid MinimumGuid = MinimumElement->Element.Mapping->Guid;
while(CurrentElement)
{
ElementCount++;
if (CurrentElement->Element.Mapping->Guid < MinimumGuid)
{
MinimumGuid = CurrentElement->Element.Mapping->Guid;
MinimumElementLink = PreviousElement;
MinimumElement = CurrentElement;
}
PreviousElement = CurrentElement;
CurrentElement = CurrentElement->Next;
}
// Slice and dice the list to put the minimum at the head before we continue
if (MinimumElementLink != NULL)
{
MinimumElementLink->Next = MinimumElement->Next;
MinimumElement->Next = LocalFirstElement;
LocalFirstElement = MinimumElement;
}
// Traverse the local list and export
CurrentElement = LocalFirstElement;
// Start exporting, planning to put everything into one file
bool bUseUniqueChannel = true;
if (LightingSystem.GetExporter().BeginExportResults(CurrentElement->Element, ElementCount) >= 0)
{
// We opened a group channel, export all mappings out together
bUseUniqueChannel = false;
}
const double ExportTimeStart = FPlatformTime::Seconds();
while(CurrentElement)
{
// write back to Unreal
LightingSystem.GetExporter().ExportResults(CurrentElement->Element, bUseUniqueChannel);
// Update the corresponding statistics depending on whether we're exporting in parallel to the worker threads or not.
bool bIsRunningInParallel = GStatistics.NumThreadsFinished < (GStatistics.NumThreads-1);
if ( bIsRunningInParallel )
{
GStatistics.ThreadStatistics.ExportTime += FPlatformTime::Seconds() - ExportTimeStart;
}
else
{
static bool bFirst = true;
if ( bFirst )
{
bFirst = false;
GSwarm->SendMessage( NSwarm::FTimingMessage( NSwarm::PROGSTATE_ExportingResults, -1 ) );
}
GStatistics.ExtraExportTime += FPlatformTime::Seconds() - ExportTimeStart;
}
GStatistics.NumExportedMappings++;
// Move to the next element
CurrentElement = CurrentElement->Next;
}
// If we didn't use unique channels, close up the group channel now
if (!bUseUniqueChannel)
{
LightingSystem.GetExporter().EndExportResults();
}
// Traverse again, cleaning up and notifying swarm
FLightmassSwarm* Swarm = LightingSystem.GetExporter().GetSwarm();
CurrentElement = LocalFirstElement;
while(CurrentElement)
{
// Tell Swarm the task is complete (if we're not in debugging mode).
if ( !LightingSystem.IsDebugMode() )
{
Swarm->TaskCompleted( CurrentElement->Element.Mapping->Guid );
}
// Delete this link and advance to the next.
TList<StaticLightingDataType>* NextElement = CurrentElement->Next;
delete CurrentElement;
CurrentElement = NextElement;
}
}
}
template<typename DataType>
void TCompleteTaskList<DataType>::ApplyAndClear(FStaticLightingSystem& LightingSystem)
{
while(this->FirstElement)
{
// Atomically read the complete list and clear the shared head pointer.
TList<DataType>* LocalFirstElement;
TList<DataType>* CurrentElement;
uint32 ElementCount = 0;
do { LocalFirstElement = this->FirstElement; }
while(FPlatformAtomics::InterlockedCompareExchangePointer((void**)&this->FirstElement,NULL,LocalFirstElement) != LocalFirstElement);
// Traverse the local list, count the number of entries, and find the minimum guid
TList<DataType>* PreviousElement = NULL;
TList<DataType>* MinimumElementLink = NULL;
TList<DataType>* MinimumElement = NULL;
CurrentElement = LocalFirstElement;
MinimumElement = CurrentElement;
FGuid MinimumGuid = MinimumElement->Element.Guid;
while(CurrentElement)
{
ElementCount++;
if (CurrentElement->Element.Guid < MinimumGuid)
{
MinimumGuid = CurrentElement->Element.Guid;
MinimumElementLink = PreviousElement;
MinimumElement = CurrentElement;
}
PreviousElement = CurrentElement;
CurrentElement = CurrentElement->Next;
}
// Slice and dice the list to put the minimum at the head before we continue
if (MinimumElementLink != NULL)
{
MinimumElementLink->Next = MinimumElement->Next;
MinimumElement->Next = LocalFirstElement;
LocalFirstElement = MinimumElement;
}
// Traverse the local list and export
CurrentElement = LocalFirstElement;
const double ExportTimeStart = FPlatformTime::Seconds();
while(CurrentElement)
{
// write back to Unreal
LightingSystem.GetExporter().ExportResults(CurrentElement->Element);
// Move to the next element
CurrentElement = CurrentElement->Next;
}
// Traverse again, cleaning up and notifying swarm
FLightmassSwarm* Swarm = LightingSystem.GetExporter().GetSwarm();
CurrentElement = LocalFirstElement;
while(CurrentElement)
{
// Tell Swarm the task is complete (if we're not in debugging mode).
if ( !LightingSystem.IsDebugMode() )
{
Swarm->TaskCompleted( CurrentElement->Element.Guid );
}
// Delete this link and advance to the next.
TList<DataType>* NextElement = CurrentElement->Next;
delete CurrentElement;
CurrentElement = NextElement;
}
}
}
class FStoredLightingSample
{
public:
FLinearColor IncomingRadiance;
FVector4f WorldSpaceDirection;
};
class FSampleCollector
{
public:
inline void SetOcclusion(float InOcclusion)
{}
inline void AddIncomingRadiance(const FLinearColor& IncomingRadiance, float Weight, const FVector4f& TangentSpaceDirection, const FVector4f& WorldSpaceDirection)
{
if (FLinearColorUtils::LinearRGBToXYZ(IncomingRadiance * Weight).G > DELTA)
{
FStoredLightingSample NewSample;
NewSample.IncomingRadiance = IncomingRadiance * Weight;
NewSample.WorldSpaceDirection = WorldSpaceDirection;
Samples.Add(NewSample);
}
}
bool AreFloatsValid() const
{
return true;
}
FSampleCollector operator+( const FSampleCollector& Other ) const
{
FSampleCollector NewCollector;
NewCollector.Samples = Samples;
NewCollector.Samples.Append(Other.Samples);
NewCollector.EnvironmentSamples = EnvironmentSamples;
NewCollector.EnvironmentSamples.Append(Other.EnvironmentSamples);
return NewCollector;
}
TArray<FStoredLightingSample> Samples;
TArray<FStoredLightingSample> EnvironmentSamples;
};
void FStaticLightingSystem::CalculateStaticShadowDepthMap(FGuid LightGuid)
{
const FLight* Light = Scene.FindLightByGuid(LightGuid);
check(Light);
const FDirectionalLight* DirectionalLight = Light->GetDirectionalLight();
const FSpotLight* SpotLight = Light->GetSpotLight();
const FPointLight* PointLight = Light->GetPointLight();
check(DirectionalLight || SpotLight || PointLight);
const float ClampedResolutionScale = FMath::Clamp(Light->ShadowResolutionScale, .125f, 8.0f);
const double StartTime = FPlatformTime::Seconds();
FStaticLightingMappingContext Context(NULL, *this);
FStaticShadowDepthMap* ShadowDepthMap = new FStaticShadowDepthMap();
if (DirectionalLight)
{
FVector4f XAxis, YAxis;
DirectionalLight->Direction.FindBestAxisVectors3(XAxis, YAxis);
// Create a coordinate system for the dominant directional light, with the z axis corresponding to the light's direction
ShadowDepthMap->WorldToLight = FBasisVectorMatrix44f(XAxis, YAxis, DirectionalLight->Direction, FVector4f(0,0,0));
FBoxSphereBounds3f ImportanceVolume = GetImportanceBounds().SphereRadius > 0.0f ? GetImportanceBounds() : FBoxSphereBounds3f(AggregateMesh->GetBounds());
const FBox3f LightSpaceImportanceBounds = ImportanceVolume.GetBox().TransformBy(ShadowDepthMap->WorldToLight);
ShadowDepthMap->ShadowMapSizeX = FMath::TruncToInt(FMath::Max(LightSpaceImportanceBounds.GetExtent().X * 2.0f * ClampedResolutionScale / ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceX, 4.0f));
ShadowDepthMap->ShadowMapSizeX = ShadowDepthMap->ShadowMapSizeX == appTruncErrorCode ? INT_MAX : ShadowDepthMap->ShadowMapSizeX;
ShadowDepthMap->ShadowMapSizeY = FMath::TruncToInt(FMath::Max(LightSpaceImportanceBounds.GetExtent().Y * 2.0f * ClampedResolutionScale / ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceY, 4.0f));
ShadowDepthMap->ShadowMapSizeY = ShadowDepthMap->ShadowMapSizeY == appTruncErrorCode ? INT_MAX : ShadowDepthMap->ShadowMapSizeY;
// Clamp the number of dominant shadow samples generated if necessary while maintaining aspect ratio
if ((uint64)ShadowDepthMap->ShadowMapSizeX * (uint64)ShadowDepthMap->ShadowMapSizeY > (uint64)ShadowSettings.StaticShadowDepthMapMaxSamples)
{
const float AspectRatio = ShadowDepthMap->ShadowMapSizeX / (float)ShadowDepthMap->ShadowMapSizeY;
ShadowDepthMap->ShadowMapSizeY = FMath::TruncToInt(FMath::Sqrt(ShadowSettings.StaticShadowDepthMapMaxSamples / AspectRatio));
ShadowDepthMap->ShadowMapSizeX = FMath::TruncToInt((float)ShadowSettings.StaticShadowDepthMapMaxSamples / (float)ShadowDepthMap->ShadowMapSizeY);
}
// Allocate the shadow map
ShadowDepthMap->ShadowMap.Empty(ShadowDepthMap->ShadowMapSizeX * ShadowDepthMap->ShadowMapSizeY);
ShadowDepthMap->ShadowMap.AddZeroed(ShadowDepthMap->ShadowMapSizeX * ShadowDepthMap->ShadowMapSizeY);
{
const float InvDistanceRange = 1.0f / (LightSpaceImportanceBounds.Max.Z - LightSpaceImportanceBounds.Min.Z);
const FMatrix44f LightToWorld = ShadowDepthMap->WorldToLight.InverseFast();
for (int32 Y = 0; Y < ShadowDepthMap->ShadowMapSizeY; Y++)
{
for (int32 X = 0; X < ShadowDepthMap->ShadowMapSizeX; X++)
{
float MaxSampleDistance = 0.0f;
// Super sample each cell
for (int32 SubSampleY = 0; SubSampleY < ShadowSettings.StaticShadowDepthMapSuperSampleFactor; SubSampleY++)
{
const float YFraction = (Y + SubSampleY / (float)ShadowSettings.StaticShadowDepthMapSuperSampleFactor) / (float)(ShadowDepthMap->ShadowMapSizeY - 1);
for (int32 SubSampleX = 0; SubSampleX < ShadowSettings.StaticShadowDepthMapSuperSampleFactor; SubSampleX++)
{
const float XFraction = (X + SubSampleX / (float)ShadowSettings.StaticShadowDepthMapSuperSampleFactor) / (float)(ShadowDepthMap->ShadowMapSizeX - 1);
// Construct a ray in light space along the direction of the light, starting at the minimum light space Z going to the maximum.
const FVector4f LightSpaceStartPosition(
LightSpaceImportanceBounds.Min.X + XFraction * (LightSpaceImportanceBounds.Max.X - LightSpaceImportanceBounds.Min.X),
LightSpaceImportanceBounds.Min.Y + YFraction * (LightSpaceImportanceBounds.Max.Y - LightSpaceImportanceBounds.Min.Y),
LightSpaceImportanceBounds.Min.Z);
const FVector4f LightSpaceEndPosition(LightSpaceStartPosition.X, LightSpaceStartPosition.Y, LightSpaceImportanceBounds.Max.Z);
// Transform the ray into world space in order to trace against the world space aggregate mesh
const FVector4f WorldSpaceStartPosition = LightToWorld.TransformPosition(LightSpaceStartPosition);
const FVector4f WorldSpaceEndPosition = LightToWorld.TransformPosition(LightSpaceEndPosition);
const FLightRay LightRay(
WorldSpaceStartPosition,
WorldSpaceEndPosition,
NULL,
NULL,
// We are tracing from the light instead of to the light,
// So flip sidedness so that backface culling matches up with tracing to the light
LIGHTRAY_FLIP_SIDEDNESS
);
FLightRayIntersection Intersection;
AggregateMesh->IntersectLightRay(LightRay, true, false, true, Context.RayCache, Intersection);
if (Intersection.bIntersects)
{
// Use the maximum distance of all super samples for each cell, to get a conservative shadow map
MaxSampleDistance = FMath::Max(MaxSampleDistance, (Intersection.IntersectionVertex.WorldPosition - WorldSpaceStartPosition).Size3());
}
}
}
if (MaxSampleDistance == 0.0f)
{
MaxSampleDistance = LightSpaceImportanceBounds.Max.Z - LightSpaceImportanceBounds.Min.Z;
}
ShadowDepthMap->ShadowMap[Y * ShadowDepthMap->ShadowMapSizeX + X] = FStaticShadowDepthMapSample(FFloat16(MaxSampleDistance * InvDistanceRange));
}
}
}
ShadowDepthMap->WorldToLight *= FTranslationMatrix44f(-LightSpaceImportanceBounds.Min)
* FScaleMatrix44f(FVector3f(1.0f) / (LightSpaceImportanceBounds.Max - LightSpaceImportanceBounds.Min));
FScopeLock Lock(&CompletedStaticShadowDepthMapsSync);
CompletedStaticShadowDepthMaps.Add(DirectionalLight, ShadowDepthMap);
}
else if (SpotLight)
{
FVector4f XAxis, YAxis;
SpotLight->Direction.FindBestAxisVectors3(XAxis, YAxis);
// Create a coordinate system for the spot light, with the z axis corresponding to the light's direction, and translated to the light's origin
ShadowDepthMap->WorldToLight = FTranslationMatrix44f(-SpotLight->Position)
* FBasisVectorMatrix44f(XAxis, YAxis, SpotLight->Direction, FVector4f(0,0,0));
// Distance from the light's direction axis to the edge of the cone at the radius of the light
const float HalfCrossSectionLength = SpotLight->Radius * FMath::Tan(SpotLight->OuterConeAngle * (float)PI / 180.0f);
const FVector4f LightSpaceImportanceBoundMin = FVector4f(-HalfCrossSectionLength, -HalfCrossSectionLength, 0);
const FVector4f LightSpaceImportanceBoundMax = FVector4f(HalfCrossSectionLength, HalfCrossSectionLength, SpotLight->Radius);
ShadowDepthMap->ShadowMapSizeX = FMath::TruncToInt(FMath::Max(HalfCrossSectionLength * ClampedResolutionScale / ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceX, 4.0f));
ShadowDepthMap->ShadowMapSizeX = ShadowDepthMap->ShadowMapSizeX == appTruncErrorCode ? INT_MAX : ShadowDepthMap->ShadowMapSizeX;
ShadowDepthMap->ShadowMapSizeY = ShadowDepthMap->ShadowMapSizeX;
// Clamp the number of dominant shadow samples generated if necessary while maintaining aspect ratio
if ((uint64)ShadowDepthMap->ShadowMapSizeX * (uint64)ShadowDepthMap->ShadowMapSizeY > (uint64)ShadowSettings.StaticShadowDepthMapMaxSamples)
{
const float AspectRatio = ShadowDepthMap->ShadowMapSizeX / (float)ShadowDepthMap->ShadowMapSizeY;
ShadowDepthMap->ShadowMapSizeY = FMath::TruncToInt(FMath::Sqrt(ShadowSettings.StaticShadowDepthMapMaxSamples / AspectRatio));
ShadowDepthMap->ShadowMapSizeX = FMath::TruncToInt((float)ShadowSettings.StaticShadowDepthMapMaxSamples / (float)ShadowDepthMap->ShadowMapSizeY);
}
ShadowDepthMap->ShadowMap.Empty(ShadowDepthMap->ShadowMapSizeX * ShadowDepthMap->ShadowMapSizeY);
ShadowDepthMap->ShadowMap.AddZeroed(ShadowDepthMap->ShadowMapSizeX * ShadowDepthMap->ShadowMapSizeY);
// Calculate the maximum possible distance for quantization
const float MaxPossibleDistance = LightSpaceImportanceBoundMax.Z - LightSpaceImportanceBoundMin.Z;
const FMatrix44f LightToWorld = ShadowDepthMap->WorldToLight.InverseFast();
const FBoxSphereBounds3f ImportanceVolume = GetImportanceBounds().SphereRadius > 0.0f ? GetImportanceBounds() : FBoxSphereBounds3f(AggregateMesh->GetBounds());
for (int32 Y = 0; Y < ShadowDepthMap->ShadowMapSizeY; Y++)
{
for (int32 X = 0; X < ShadowDepthMap->ShadowMapSizeX; X++)
{
float MaxSampleDistance = 0.0f;
// Super sample each cell
for (int32 SubSampleY = 0; SubSampleY < ShadowSettings.StaticShadowDepthMapSuperSampleFactor; SubSampleY++)
{
const float YFraction = (Y + SubSampleY / (float)ShadowSettings.StaticShadowDepthMapSuperSampleFactor) / (float)(ShadowDepthMap->ShadowMapSizeY - 1);
for (int32 SubSampleX = 0; SubSampleX < ShadowSettings.StaticShadowDepthMapSuperSampleFactor; SubSampleX++)
{
const float XFraction = (X + SubSampleX / (float)ShadowSettings.StaticShadowDepthMapSuperSampleFactor) / (float)(ShadowDepthMap->ShadowMapSizeX - 1);
// Construct a ray in light space along the direction of the light, starting at the light and going to the maximum light space Z.
const FVector4f LightSpaceStartPosition(0,0,0);
const FVector4f LightSpaceEndPosition(
LightSpaceImportanceBoundMin.X + XFraction * (LightSpaceImportanceBoundMax.X - LightSpaceImportanceBoundMin.X),
LightSpaceImportanceBoundMin.Y + YFraction * (LightSpaceImportanceBoundMax.Y - LightSpaceImportanceBoundMin.Y),
LightSpaceImportanceBoundMax.Z);
// Transform the ray into world space in order to trace against the world space aggregate mesh
const FVector4f WorldSpaceStartPosition = LightToWorld.TransformPosition(LightSpaceStartPosition);
const FVector4f WorldSpaceEndPosition = LightToWorld.TransformPosition(LightSpaceEndPosition);
const FLightRay LightRay(
WorldSpaceStartPosition,
WorldSpaceEndPosition,
NULL,
NULL,
// We are tracing from the light instead of to the light,
// So flip sidedness so that backface culling matches up with tracing to the light
LIGHTRAY_FLIP_SIDEDNESS
);
FLightRayIntersection Intersection;
AggregateMesh->IntersectLightRay(LightRay, true, false, true, Context.RayCache, Intersection);
if (Intersection.bIntersects)
{
const FVector4f LightSpaceIntersectPosition = ShadowDepthMap->WorldToLight.TransformPosition(Intersection.IntersectionVertex.WorldPosition);
// Use the maximum distance of all super samples for each cell, to get a conservative shadow map
MaxSampleDistance = FMath::Max(MaxSampleDistance, LightSpaceIntersectPosition.Z);
}
}
}
if (MaxSampleDistance == 0.0f)
{
MaxSampleDistance = MaxPossibleDistance;
}
ShadowDepthMap->ShadowMap[Y * ShadowDepthMap->ShadowMapSizeX + X] = FStaticShadowDepthMapSample(FFloat16(MaxSampleDistance / MaxPossibleDistance));
}
}
ShadowDepthMap->WorldToLight *=
// Perspective projection sized to the spotlight cone
FPerspectiveMatrix44f(SpotLight->OuterConeAngle * (float)PI / 180.0f, 1, 1, 0, SpotLight->Radius)
// Convert from NDC to texture space, normalize Z
* FMatrix44f(
FPlane4f(.5f, 0, 0, 0),
FPlane4f(0, .5f, 0, 0),
FPlane4f(0, 0, 1.0f / LightSpaceImportanceBoundMax.Z, 0),
FPlane4f(.5f, .5f, 0, 1));
FScopeLock Lock(&CompletedStaticShadowDepthMapsSync);
CompletedStaticShadowDepthMaps.Add(SpotLight, ShadowDepthMap);
}
else if (PointLight)
{
ShadowDepthMap->ShadowMapSizeX = FMath::TruncToInt(FMath::Max(PointLight->Radius * 4 * ClampedResolutionScale / ShadowSettings.StaticShadowDepthMapTransitionSampleDistanceX, 4.0f));
ShadowDepthMap->ShadowMapSizeX = ShadowDepthMap->ShadowMapSizeX == appTruncErrorCode ? INT_MAX : ShadowDepthMap->ShadowMapSizeX;
ShadowDepthMap->ShadowMapSizeY = ShadowDepthMap->ShadowMapSizeX;
// Clamp the number of dominant shadow samples generated if necessary while maintaining aspect ratio
if ((uint64)ShadowDepthMap->ShadowMapSizeX * (uint64)ShadowDepthMap->ShadowMapSizeY > (uint64)ShadowSettings.StaticShadowDepthMapMaxSamples)
{
const float AspectRatio = ShadowDepthMap->ShadowMapSizeX / (float)ShadowDepthMap->ShadowMapSizeY;
ShadowDepthMap->ShadowMapSizeY = FMath::TruncToInt(FMath::Sqrt(ShadowSettings.StaticShadowDepthMapMaxSamples / AspectRatio));
ShadowDepthMap->ShadowMapSizeX = FMath::TruncToInt((float)ShadowSettings.StaticShadowDepthMapMaxSamples / (float)ShadowDepthMap->ShadowMapSizeY);
}
// Allocate the shadow map
ShadowDepthMap->ShadowMap.Empty(ShadowDepthMap->ShadowMapSizeX * ShadowDepthMap->ShadowMapSizeY);
ShadowDepthMap->ShadowMap.AddZeroed(ShadowDepthMap->ShadowMapSizeX * ShadowDepthMap->ShadowMapSizeY);
ShadowDepthMap->WorldToLight = FMatrix44f::Identity;
for (int32 Y = 0; Y < ShadowDepthMap->ShadowMapSizeY; Y++)
{
for (int32 X = 0; X < ShadowDepthMap->ShadowMapSizeX; X++)
{
float MaxSampleDistance = 0.0f;
// Super sample each cell
for (int32 SubSampleY = 0; SubSampleY < ShadowSettings.StaticShadowDepthMapSuperSampleFactor; SubSampleY++)
{
const float YFraction = (Y + SubSampleY / (float)ShadowSettings.StaticShadowDepthMapSuperSampleFactor) / (float)(ShadowDepthMap->ShadowMapSizeY - 1);
const float Phi = YFraction * PI;
const float SinPhi = FMath::Sin(Phi);
for (int32 SubSampleX = 0; SubSampleX < ShadowSettings.StaticShadowDepthMapSuperSampleFactor; SubSampleX++)
{
const float XFraction = (X + SubSampleX / (float)ShadowSettings.StaticShadowDepthMapSuperSampleFactor) / (float)(ShadowDepthMap->ShadowMapSizeX - 1);
const float Theta = XFraction * 2 * PI;
const FVector3f Direction(FMath::Cos(Theta) * SinPhi, FMath::Sin(Theta) * SinPhi, FMath::Cos(Phi));
const FVector4f WorldSpaceStartPosition = PointLight->Position;
const FVector4f WorldSpaceEndPosition = PointLight->Position + Direction * PointLight->Radius;
const FLightRay LightRay(
WorldSpaceStartPosition,
WorldSpaceEndPosition,
NULL,
NULL,
// We are tracing from the light instead of to the light,
// So flip sidedness so that backface culling matches up with tracing to the light
LIGHTRAY_FLIP_SIDEDNESS
);
FLightRayIntersection Intersection;
AggregateMesh->IntersectLightRay(LightRay, true, false, true, Context.RayCache, Intersection);
if (Intersection.bIntersects)
{
// Use the maximum distance of all super samples for each cell, to get a conservative shadow map
MaxSampleDistance = FMath::Max(MaxSampleDistance, (Intersection.IntersectionVertex.WorldPosition - PointLight->Position).Size3());
}
}
}
if (MaxSampleDistance == 0.0f)
{
MaxSampleDistance = PointLight->Radius;
}
ShadowDepthMap->ShadowMap[Y * ShadowDepthMap->ShadowMapSizeX + X] = FStaticShadowDepthMapSample(FFloat16(MaxSampleDistance / PointLight->Radius));
}
}
FScopeLock Lock(&CompletedStaticShadowDepthMapsSync);
CompletedStaticShadowDepthMaps.Add(PointLight, ShadowDepthMap);
}
const float NewTime = FPlatformTime::Seconds() - StartTime;
Context.Stats.StaticShadowDepthMapThreadTime = NewTime;
Context.Stats.MaxStaticShadowDepthMapThreadTime = NewTime;
}
/**
* Calculates shadowing for a given mapping surface point and light.
* @param Mapping - The mapping the point comes from.
* @param WorldSurfacePoint - The point to check shadowing at.
* @param Light - The light to check shadowing from.
* @param CoherentRayCache - The calling thread's collision cache.
* @return true if the surface point is shadowed from the light.
*/
bool FStaticLightingSystem::CalculatePointShadowing(
const FStaticLightingMapping* Mapping,
const FVector4f& WorldSurfacePoint,
const FLight* Light,
FStaticLightingMappingContext& MappingContext,
bool bDebugThisSample
) const
{
if(Light->GetSkyLight())
{
return true;
}
else
{
// Treat points which the light doesn't affect as shadowed to avoid the costly ray check.
if(!Light->AffectsBounds(FBoxSphereBounds3f(WorldSurfacePoint,FVector4f(0,0,0,0),0)))
{
return true;
}
// Check for visibility between the point and the light.
bool bIsShadowed = false;
if ((Light->LightFlags & GI_LIGHT_CASTSHADOWS) && (Light->LightFlags & GI_LIGHT_CASTSTATICSHADOWS))
{
// TODO find best point on light to shadow from
// Construct a line segment between the light and the surface point.
const FVector4f LightPosition = FVector4f(Light->Position.X, Light->Position.Y, Light->Position.Z, 0);
const FVector4f LightVector = LightPosition - WorldSurfacePoint * Light->Position.W;
const FLightRay LightRay(
WorldSurfacePoint + LightVector.GetSafeNormal() * SceneConstants.VisibilityRayOffsetDistance,
WorldSurfacePoint + LightVector,
Mapping,
Light
);
// Check the line segment for intersection with the static lighting meshes.
FLightRayIntersection Intersection;
AggregateMesh->IntersectLightRay(LightRay, false, false, true, MappingContext.RayCache, Intersection);
bIsShadowed = Intersection.bIntersects;
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
if (bDebugThisSample)
{
FDebugStaticLightingRay DebugRay(LightRay.Start, LightRay.End, bIsShadowed != 0);
if (bIsShadowed)
{
DebugRay.End = Intersection.IntersectionVertex.WorldPosition;
}
MappingContext.DebugOutput->ShadowRays.Add(DebugRay);
}
#endif
}
return bIsShadowed;
}
}
/** Calculates area shadowing from a light for the given vertex. */
FVector2f FStaticLightingSystem::CalculatePointAreaShadowing(
const FStaticLightingMapping* Mapping,
const FStaticLightingVertex& Vertex,
int32 ElementIndex,
float SampleRadius,
const FLight* Light,
FStaticLightingMappingContext& MappingContext,
FLMRandomStream& RandomStream,
FLinearColor& UnnormalizedTransmission,
const TArray<FLightSurfaceSample>& LightPositionSamples,
bool bDebugThisSample
) const
{
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
if (bDebugThisSample)
{
int32 TempBreak = 0;
}
#endif
UnnormalizedTransmission = FLinearColor::Black;
// Treat points which the light doesn't affect as shadowed to avoid the costly ray check.
if( !Light->AffectsBounds(FBoxSphereBounds3f(Vertex.WorldPosition,FVector4f(0,0,0),0)))
{
return FVector2f::ZeroVector;
}
// Check for visibility between the point and the light
if ((Light->LightFlags & GI_LIGHT_CASTSHADOWS) && (Light->LightFlags & GI_LIGHT_CASTSTATICSHADOWS))
{
MappingContext.Stats.NumDirectLightingShadowRays += LightPositionSamples.Num();
const bool bIsTwoSided = Mapping->Mesh->IsTwoSided(ElementIndex);
FVector2f ShadowedValue = FVector2f::ZeroVector;
// Integrate over the surface of the light using monte carlo integration
// Note that we are making the approximation that the BRDF and the Light's emission are equal in all of these directions and therefore are not in the integrand
for(int32 RayIndex = 0; RayIndex < LightPositionSamples.Num(); RayIndex++)
{
FLightSurfaceSample CurrentSample = LightPositionSamples[RayIndex];
// Allow the light to modify the surface position for this receiving position
Light->ValidateSurfaceSample(Vertex.WorldPosition, CurrentSample);
// Construct a line segment between the light and the surface point.
const FVector4f LightVector = CurrentSample.Position - Vertex.WorldPosition;
FVector4f SampleOffset(0,0,0);
if (GeneralSettings.bAccountForTexelSize)
{
/*
//@todo - the rays cross over on the way to the light and mess up penumbra shapes.
//@todo - need to use more than texel size, otherwise BSP generates lots of texels that become half shadowed at corners
SampleOffset = Vertex.WorldTangentX * LightPositionSamples(RayIndex).DiskPosition.X * SampleRadius * SceneConstants.VisibilityTangentOffsetSampleRadiusScale
+ Vertex.WorldTangentY * LightPositionSamples(RayIndex).DiskPosition.Y * SampleRadius * SceneConstants.VisibilityTangentOffsetSampleRadiusScale;
*/
}
float DistSqr = LightVector.SizeSquared3() + KINDA_SMALL_NUMBER;
float SamplePDF = CurrentSample.PDF * -Dot3( CurrentSample.Normal, LightVector ) / ( DistSqr * FMath::Sqrt( DistSqr ) );
if( SamplePDF <= 0.0f )
{
continue;
}
FVector4f NormalForOffset = Vertex.WorldTangentZ;
// Flip the normal used for offsetting the start of the ray for two sided materials if a flipped normal would be closer to the light.
// This prevents incorrect shadowing where using the frontface normal would cause the ray to start inside a nearby object.
if (bIsTwoSided && Dot3(-NormalForOffset, LightVector) > Dot3(NormalForOffset, LightVector))
{
NormalForOffset = -NormalForOffset;
}
const FLightRay LightRay(
// Offset the start of the ray by some fraction along the direction of the ray and some fraction along the vertex normal.
Vertex.WorldPosition
+ LightVector.GetSafeNormal() * SceneConstants.VisibilityRayOffsetDistance
+ NormalForOffset * SampleRadius * SceneConstants.VisibilityNormalOffsetSampleRadiusScale
+ SampleOffset,
Vertex.WorldPosition + LightVector,
Mapping,
Light
);
// Check the line segment for intersection with the static lighting meshes.
FLightRayIntersection Intersection;
//@todo - change this back to request boolean visibility once transmission is supported with boolean visibility ray intersections
AggregateMesh->IntersectLightRay(LightRay, true, true, true, MappingContext.RayCache, Intersection);
if (!Intersection.bIntersects)
{
UnnormalizedTransmission += Intersection.Transmission * SamplePDF;
ShadowedValue.X += SamplePDF;
}
ShadowedValue.Y += SamplePDF;
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
if (bDebugThisSample)
{
FDebugStaticLightingRay DebugRay(LightRay.Start, LightRay.End, Intersection.bIntersects);
if (Intersection.bIntersects)
{
DebugRay.End = Intersection.IntersectionVertex.WorldPosition;
}
MappingContext.DebugOutput->ShadowRays.Add(DebugRay);
}
#endif
}
return ShadowedValue;
}
UnnormalizedTransmission = FLinearColor::White;
return FVector2f( 1.0f, 1.0f );
}
/** Calculates the lighting contribution of a light to a mapping vertex. */
FGatheredLightSample FStaticLightingSystem::CalculatePointLighting(
const FStaticLightingMapping* Mapping,
const FStaticLightingVertex& Vertex,
int32 ElementIndex,
const FLight* Light,
const FLinearColor& InLightIntensity,
const FLinearColor& InTransmission
) const
{
// don't do sky lights here
if (Light->GetSkyLight() == NULL)
{
// Calculate the direction from the vertex to the light.
const FVector4f WorldLightVector = Light->GetDirectLightingDirection(Vertex.WorldPosition, Vertex.WorldTangentZ);
// Transform the light vector to tangent space.
const FVector4f TangentLightVector =
FVector4f(
Dot3(WorldLightVector, Vertex.WorldTangentX),
Dot3(WorldLightVector, Vertex.WorldTangentY),
Dot3(WorldLightVector, Vertex.WorldTangentZ),
0
).GetSafeNormal();
// Compute the incident lighting of the light on the vertex.
const FLinearColor LightIntensity = InLightIntensity * InTransmission;
// Compute the light-map sample for the front-face of the vertex.
FGatheredLightSample FrontFaceSample = FGatheredLightSampleUtil::PointLightWorldSpace<2>(LightIntensity, TangentLightVector, WorldLightVector.GetSafeNormal());
if (Mapping->Mesh->UsesTwoSidedLighting(ElementIndex))
{
const FVector4f BackFaceTangentLightVector =
FVector4f(
Dot3(WorldLightVector, -Vertex.WorldTangentX),
Dot3(WorldLightVector, -Vertex.WorldTangentY),
Dot3(WorldLightVector, -Vertex.WorldTangentZ),
0
).GetSafeNormal();
const FGatheredLightSample BackFaceSample = FGatheredLightSampleUtil::PointLightWorldSpace<2>(LightIntensity, BackFaceTangentLightVector, -WorldLightVector.GetSafeNormal());
// Average front and back face lighting
return (FrontFaceSample + BackFaceSample) * .5f;
}
else
{
return FrontFaceSample;
}
}
return FGatheredLightSample();
}
/** Returns a light sample that represents the material attribute specified by MaterialSettings.ViewMaterialAttribute at the intersection. */
FGatheredLightSample FStaticLightingSystem::GetVisualizedMaterialAttribute(const FStaticLightingMapping* Mapping, const FLightRayIntersection& Intersection) const
{
FGatheredLightSample MaterialSample;
if (Intersection.bIntersects && Intersection.Mapping == Mapping)
{
// The ray intersected an opaque surface, we can visualize anything that opaque materials store
//@todo - Currently can't visualize emissive on translucent materials
if (MaterialSettings.ViewMaterialAttribute == VMA_Emissive)
{
FLinearColor Emissive = FLinearColor::Black;
if (Intersection.Mesh->IsEmissive(Intersection.ElementIndex))
{
Emissive = Intersection.Mesh->EvaluateEmissive(Intersection.IntersectionVertex.TextureCoordinates[0], Intersection.ElementIndex);
}
MaterialSample = FGatheredLightSampleUtil::AmbientLight<2>(Emissive);
}
else if (MaterialSettings.ViewMaterialAttribute == VMA_Diffuse)
{
const FLinearColor Diffuse = Intersection.Mesh->EvaluateDiffuse(Intersection.IntersectionVertex.TextureCoordinates[0], Intersection.ElementIndex);
MaterialSample = FGatheredLightSampleUtil::AmbientLight<2>(Diffuse);
}
else if (MaterialSettings.ViewMaterialAttribute == VMA_Normal)
{
const FVector4f Normal = Intersection.Mesh->EvaluateNormal(Intersection.IntersectionVertex.TextureCoordinates[0], Intersection.ElementIndex);
FLinearColor NormalColor;
NormalColor.R = Normal.X * 0.5f + 0.5f;
NormalColor.G = Normal.Y * 0.5f + 0.5f;
NormalColor.B = Normal.Z * 0.5f + 0.5f;
NormalColor.A = 1.0f;
MaterialSample = FGatheredLightSampleUtil::AmbientLight<2>(NormalColor);
}
}
else if (MaterialSettings.ViewMaterialAttribute != VMA_Transmission)
{
// The ray didn't intersect an opaque surface and we're not visualizing transmission
MaterialSample = FGatheredLightSampleUtil::AmbientLight<2>(FLinearColor::Black);
}
if (MaterialSettings.ViewMaterialAttribute == VMA_Transmission)
{
// Visualizing transmission, replace the light sample with the transmission picked up along the ray
MaterialSample = FGatheredLightSampleUtil::AmbientLight<2>(Intersection.Transmission);
}
return MaterialSample;
}
/**
* Checks if a light is behind a triangle.
* @param TrianglePoint - Any point on the triangle.
* @param TriangleNormal - The (not necessarily normalized) triangle surface normal.
* @param Light - The light to classify.
* @return true if the light is behind the triangle.
*/
bool IsLightBehindSurface(const FVector4f& TrianglePoint, const FVector4f& TriangleNormal, const FLight* Light)
{
const bool bIsSkyLight = Light->GetSkyLight() != NULL;
if (!bIsSkyLight)
{
// Calculate the direction from the triangle to the light.
const FVector4f LightPosition = FVector4f(Light->Position.X, Light->Position.Y, Light->Position.Z, 0);
const FVector4f WorldLightVector = LightPosition - TrianglePoint * Light->Position.W;
// Check if the light is in front of the triangle.
const float Dot = Dot3(WorldLightVector, TriangleNormal);
return Dot < 0.0f;
}
else
{
// Sky lights are always in front of a surface.
return false;
}
}
/**
* Culls lights that are behind a triangle.
* @param bTwoSidedMaterial - true if the triangle has a two-sided material. If so, lights behind the surface are not culled.
* @param TrianglePoint - Any point on the triangle.
* @param TriangleNormal - The (not necessarily normalized) triangle surface normal.
* @param Lights - The lights to cull.
* @return A map from Lights index to a boolean which is true if the light is in front of the triangle.
*/
TBitArray<> CullBackfacingLights(bool bTwoSidedMaterial,const FVector4f& TrianglePoint,const FVector4f& TriangleNormal,const TArray<FLight*>& Lights)
{
if(!bTwoSidedMaterial)
{
TBitArray<> Result(false,Lights.Num());
for(int32 LightIndex = 0;LightIndex < Lights.Num();LightIndex++)
{
Result[LightIndex] = !IsLightBehindSurface(TrianglePoint,TriangleNormal,Lights[LightIndex]);
}
return Result;
}
else
{
return TBitArray<>(true,Lights.Num());
}
}
} //namespace Lightmass