3804 lines
162 KiB
C++
3804 lines
162 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "Raster.h"
|
|
#include "LightingSystem.h"
|
|
#include "LightmassSwarm.h"
|
|
#include "HAL/RunnableThread.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "TextureMappingSetup.h"
|
|
|
|
namespace Lightmass
|
|
{
|
|
|
|
void FStaticLightingRasterPolicy::ProcessPixel(int32 X,int32 Y,const InterpolantType& Interpolant,bool BackFacing)
|
|
{
|
|
FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& X == Scene.DebugInput.LocalX
|
|
&& Y == Scene.DebugInput.LocalY)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
|
|
if (bUseMaxWeight)
|
|
{
|
|
if (SampleWeight > TexelToVertex.MaxSampleWeight)
|
|
{
|
|
// Use the sample with the largest weight.
|
|
// This has the disadvantage compared averaging based on weight that it won't be well centered for texels on a UV seam,
|
|
// But it has the advantage that the final position is guaranteed to be valid (ie actually on a triangle),
|
|
// Even for split texels which are mapped to triangles in different parts of the mesh.
|
|
TexelToVertex.MaxSampleWeight = SampleWeight;
|
|
TexelToVertex.WorldPosition = Interpolant.Vertex.WorldPosition;
|
|
TexelToVertex.ElementIndex = Interpolant.ElementIndex;
|
|
|
|
for( int32 CurCoordIndex = 0; CurCoordIndex < MAX_TEXCOORDS; ++CurCoordIndex )
|
|
{
|
|
TexelToVertex.TextureCoordinates[ CurCoordIndex ] = Interpolant.Vertex.TextureCoordinates[ CurCoordIndex ];
|
|
}
|
|
}
|
|
|
|
// Weighted average of normal, improves the case where the position chosen by the max weight has a different normal than the rest of the texel
|
|
// Eg, small extrusions from an otherwise flat surface, and the texel center lies on the perpendicular extrusion
|
|
//@todo - only average normals within the texel radius to improve the split texel case?
|
|
TexelToVertex.WorldTangentX += Interpolant.Vertex.WorldTangentX * SampleWeight;
|
|
TexelToVertex.WorldTangentY += Interpolant.Vertex.WorldTangentY * SampleWeight;
|
|
TexelToVertex.WorldTangentZ += Interpolant.Vertex.WorldTangentZ * SampleWeight;
|
|
checkSlow(!TriangleNormal.ContainsNaN());
|
|
TexelToVertex.TriangleNormal += TriangleNormal * SampleWeight;
|
|
TexelToVertex.TotalSampleWeight += SampleWeight;
|
|
}
|
|
else if (!bUseMaxWeight)
|
|
{
|
|
// Update the sample weight, and compute the scales used to update the sample's averages.
|
|
const float NewTotalSampleWeight = TexelToVertex.TotalSampleWeight + SampleWeight;
|
|
const float OldSampleWeight = TexelToVertex.TotalSampleWeight / NewTotalSampleWeight;
|
|
const float NewSampleWeight = SampleWeight / NewTotalSampleWeight;
|
|
TexelToVertex.TotalSampleWeight = NewTotalSampleWeight;
|
|
|
|
// Add this sample to the mapping.
|
|
TexelToVertex.WorldPosition = TexelToVertex.WorldPosition * OldSampleWeight + Interpolant.Vertex.WorldPosition * NewSampleWeight;
|
|
TexelToVertex.WorldTangentX = FVector4f(TexelToVertex.WorldTangentX) * OldSampleWeight + Interpolant.Vertex.WorldTangentX * NewSampleWeight;
|
|
TexelToVertex.WorldTangentY = FVector4f(TexelToVertex.WorldTangentY) * OldSampleWeight + Interpolant.Vertex.WorldTangentY * NewSampleWeight;
|
|
TexelToVertex.WorldTangentZ = FVector4f(TexelToVertex.WorldTangentZ) * OldSampleWeight + Interpolant.Vertex.WorldTangentZ * NewSampleWeight;
|
|
TexelToVertex.TriangleNormal = TriangleNormal;
|
|
TexelToVertex.ElementIndex = Interpolant.ElementIndex;
|
|
|
|
for( int32 CurCoordIndex = 0; CurCoordIndex < MAX_TEXCOORDS; ++CurCoordIndex )
|
|
{
|
|
TexelToVertex.TextureCoordinates[ CurCoordIndex ] = TexelToVertex.TextureCoordinates[ CurCoordIndex ] * OldSampleWeight + Interpolant.Vertex.TextureCoordinates[ CurCoordIndex ] * NewSampleWeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FFullStaticLightingVertex::ApplyVertexModifications(int32 ElementIndex, bool bUseNormalMapsForLighting, const FStaticLightingMesh* Mesh)
|
|
{
|
|
if (bUseNormalMapsForLighting && Mesh->HasImportedNormal(ElementIndex))
|
|
{
|
|
const FVector4f TangentNormal = Mesh->EvaluateNormal(TextureCoordinates[0], ElementIndex);
|
|
|
|
const FVector4f WorldTangentRow0(WorldTangentX.X, WorldTangentY.X, WorldTangentZ.X);
|
|
const FVector4f WorldTangentRow1(WorldTangentX.Y, WorldTangentY.Y, WorldTangentZ.Y);
|
|
const FVector4f WorldTangentRow2(WorldTangentX.Z, WorldTangentY.Z, WorldTangentZ.Z);
|
|
const FVector4f WorldVector(
|
|
Dot3(WorldTangentRow0, TangentNormal),
|
|
Dot3(WorldTangentRow1, TangentNormal),
|
|
Dot3(WorldTangentRow2, TangentNormal)
|
|
);
|
|
|
|
WorldTangentZ = WorldVector;
|
|
}
|
|
|
|
// Normalize the tangent basis and ensure it is orthonormal
|
|
WorldTangentZ = WorldTangentZ.GetUnsafeNormal3();
|
|
|
|
const bool bUseVertexNormalForHemisphereGather = Mesh->UseVertexNormalForHemisphereGather(ElementIndex);
|
|
TriangleNormal = bUseVertexNormalForHemisphereGather ? WorldTangentZ : TriangleNormal.GetUnsafeNormal3();
|
|
checkSlow(!TriangleNormal.ContainsNaN());
|
|
|
|
const FVector4f OriginalTangentX = WorldTangentX;
|
|
const FVector4f OriginalTangentY = WorldTangentY;
|
|
|
|
WorldTangentY = (WorldTangentZ ^ WorldTangentX).GetUnsafeNormal3();
|
|
// Maintain handedness
|
|
if (Dot3(WorldTangentY, OriginalTangentY) < 0)
|
|
{
|
|
WorldTangentY *= -1.0f;
|
|
}
|
|
WorldTangentX = WorldTangentY ^ WorldTangentZ;
|
|
if (Dot3(WorldTangentX, OriginalTangentX) < 0)
|
|
{
|
|
WorldTangentX *= -1.0f;
|
|
}
|
|
}
|
|
|
|
void FStaticLightingTextureMapping::Initialize(FStaticLightingSystem& System)
|
|
{
|
|
SurfaceCacheSizeX = FMath::Max(FMath::TruncToInt((float)CachedSizeX / (float)System.GeneralSettings.MappingSurfaceCacheDownsampleFactor), 6);
|
|
SurfaceCacheSizeY = FMath::Max(FMath::TruncToInt((float)CachedSizeY / (float)System.GeneralSettings.MappingSurfaceCacheDownsampleFactor), 6);
|
|
}
|
|
|
|
/** Caches irradiance photons on a single texture mapping. */
|
|
void FStaticLightingSystem::CacheIrradiancePhotonsTextureMapping(FStaticLightingTextureMapping* TextureMapping)
|
|
{
|
|
checkSlow(TextureMapping);
|
|
FStaticLightingMappingContext MappingContext(TextureMapping->Mesh,*this,&StartupDebugOutput);
|
|
LIGHTINGSTAT(FScopedRDTSCTimer CachingTime(MappingContext.Stats.IrradiancePhotonCachingThreadTime));
|
|
const FBoxSphereBounds3f ImportanceBounds = Scene.GetImportanceBounds();
|
|
|
|
FTexelToVertexMap TexelToVertexMap(TextureMapping->SurfaceCacheSizeX, TextureMapping->SurfaceCacheSizeY);
|
|
|
|
bool bDebugThisMapping = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
bDebugThisMapping = TextureMapping == Scene.DebugMapping;
|
|
int32 IrradiancePhotonCacheDebugX = -1;
|
|
int32 IrradiancePhotonCacheDebugY = -1;
|
|
if (bDebugThisMapping)
|
|
{
|
|
IrradiancePhotonCacheDebugX = FMath::TruncToInt(Scene.DebugInput.LocalX / (float)TextureMapping->CachedSizeX * TextureMapping->SurfaceCacheSizeX);
|
|
IrradiancePhotonCacheDebugY = FMath::TruncToInt(Scene.DebugInput.LocalY / (float)TextureMapping->CachedSizeY * TextureMapping->SurfaceCacheSizeY);
|
|
}
|
|
#endif
|
|
|
|
RasterizeToSurfaceCacheTextureMapping(TextureMapping, bDebugThisMapping, TexelToVertexMap);
|
|
|
|
// Allocate space for the cached irradiance photons
|
|
TextureMapping->CachedIrradiancePhotons.Empty(TextureMapping->SurfaceCacheSizeX * TextureMapping->SurfaceCacheSizeY);
|
|
TextureMapping->CachedIrradiancePhotons.AddZeroed(TextureMapping->SurfaceCacheSizeX * TextureMapping->SurfaceCacheSizeY);
|
|
|
|
TArray<FIrradiancePhoton*> TempIrradiancePhotons;
|
|
for (int32 Y = 0; Y < TextureMapping->SurfaceCacheSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->SurfaceCacheSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == IrradiancePhotonCacheDebugY
|
|
&& X == IrradiancePhotonCacheDebugX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
if (TexelToVertex.TotalSampleWeight > 0.0f)
|
|
{
|
|
MappingContext.Stats.NumCachedIrradianceSamples++;
|
|
FFullStaticLightingVertex CurrentVertex = TexelToVertex.GetFullVertex();
|
|
|
|
CurrentVertex.ApplyVertexModifications(TexelToVertex.ElementIndex, MaterialSettings.bUseNormalMapsForLighting, TextureMapping->Mesh);
|
|
|
|
FIrradiancePhoton* NearestPhoton = NULL;
|
|
|
|
// Only search the irradiance photon map if the surface cache position is inside the importance volume,
|
|
// Since irradiance photons are only deposited inside the importance volume.
|
|
if (ImportanceBounds.GetBox().IsInside(CurrentVertex.WorldPosition))
|
|
{
|
|
// Find the nearest irradiance photon and store it on the surface of the mapping
|
|
// Only find visible irradiance photons to prevent light leaking through thin surfaces
|
|
//Note: It's still possible for light to leak if a single texel spans two disjoint lighting areas, for example two planes coming together at a 90 degree angle.
|
|
NearestPhoton = FindNearestIrradiancePhoton(CurrentVertex, MappingContext, TempIrradiancePhotons, true, bDebugThisTexel);
|
|
|
|
if (NearestPhoton)
|
|
{
|
|
if (!NearestPhoton->IsUsed())
|
|
{
|
|
// An irradiance photon was found that hadn't been marked used yet
|
|
MappingContext.Stats.NumFoundIrradiancePhotons++;
|
|
NearestPhoton->SetUsed();
|
|
}
|
|
|
|
TextureMapping->CachedIrradiancePhotons[Y * TextureMapping->SurfaceCacheSizeX + X] = NearestPhoton;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Cache irradiance photons on surfaces. */
|
|
void FStaticLightingSystem::FinalizeSurfaceCache()
|
|
{
|
|
for(int32 ThreadIndex = 1; ThreadIndex < NumStaticLightingThreads; ThreadIndex++)
|
|
{
|
|
FMappingProcessingThreadRunnable* ThreadRunnable = new FMappingProcessingThreadRunnable(this, ThreadIndex, StaticLightingTask_FinalizeSurfaceCache);
|
|
FinalizeSurfaceCacheThreads.Add(ThreadRunnable);
|
|
const FString ThreadName = FString::Printf(TEXT("FinalizeSurfaceCacheThread%u"), ThreadIndex);
|
|
ThreadRunnable->Thread = FRunnableThread::Create(ThreadRunnable, *ThreadName);
|
|
}
|
|
|
|
// Start the static lighting thread loop on the main thread, too.
|
|
// Once it returns, all static lighting mappings have begun processing.
|
|
FinalizeSurfaceCacheThreadLoop(0, true);
|
|
|
|
// Stop the static lighting threads.
|
|
for(int32 ThreadIndex = 0;ThreadIndex < FinalizeSurfaceCacheThreads.Num();ThreadIndex++)
|
|
{
|
|
// Wait for the thread to exit.
|
|
FinalizeSurfaceCacheThreads[ThreadIndex].Thread->WaitForCompletion();
|
|
// Check that it didn't terminate with an error.
|
|
FinalizeSurfaceCacheThreads[ThreadIndex].CheckHealth();
|
|
|
|
// Destroy the thread.
|
|
delete FinalizeSurfaceCacheThreads[ThreadIndex].Thread;
|
|
}
|
|
FinalizeSurfaceCacheThreads.Empty();
|
|
}
|
|
|
|
void FStaticLightingSystem::FinalizeSurfaceCacheThreadLoop(int32 ThreadIndex, bool bIsMainThread)
|
|
{
|
|
bool bIsDone = false;
|
|
while (!bIsDone)
|
|
{
|
|
// Atomically read and increment the next mapping index to process.
|
|
const int32 MappingIndex = NextMappingToFinalizeSurfaceCache.Increment() - 1;
|
|
|
|
if (MappingIndex < AllMappings.Num())
|
|
{
|
|
// If this is the main thread, update progress and apply completed static lighting.
|
|
if (bIsMainThread)
|
|
{
|
|
// Check the health of all static lighting threads.
|
|
for (int32 ThreadIndexIter = 0; ThreadIndexIter < FinalizeSurfaceCacheThreads.Num(); ThreadIndexIter++)
|
|
{
|
|
FinalizeSurfaceCacheThreads[ThreadIndexIter].CheckHealth();
|
|
}
|
|
}
|
|
|
|
FStaticLightingTextureMapping* TextureMapping = AllMappings[MappingIndex]->GetTextureMapping();
|
|
|
|
if (TextureMapping)
|
|
{
|
|
FinalizeSurfaceCacheTextureMapping(TextureMapping);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Processing has begun for all mappings.
|
|
bIsDone = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FStaticLightingSystem::RasterizeToSurfaceCacheTextureMapping(FStaticLightingTextureMapping* TextureMapping, bool bDebugThisMapping, FTexelToVertexMap& TexelToVertexMap)
|
|
{
|
|
FStaticLightingMappingContext MappingContext(TextureMapping->Mesh, *this, &StartupDebugOutput);
|
|
|
|
FTexelToCornersMap TexelToCornersMap(TextureMapping->SurfaceCacheSizeX, TextureMapping->SurfaceCacheSizeY);
|
|
CalculateTexelCorners(TextureMapping->Mesh, TexelToCornersMap, TextureMapping->LightmapTextureCoordinateIndex, bDebugThisMapping);
|
|
|
|
// Using conservative rasterization, which uses super sampling to try to detect all texels that should be mapped.
|
|
for(int32 TriangleIndex = 0;TriangleIndex < TextureMapping->Mesh->NumTriangles;TriangleIndex++)
|
|
{
|
|
// Query the mesh for the triangle's vertices.
|
|
FStaticLightingInterpolant V0;
|
|
FStaticLightingInterpolant V1;
|
|
FStaticLightingInterpolant V2;
|
|
int32 Element;
|
|
TextureMapping->Mesh->GetTriangle(TriangleIndex,V0.Vertex,V1.Vertex,V2.Vertex,Element);
|
|
V0.ElementIndex = V1.ElementIndex = V2.ElementIndex = Element;
|
|
|
|
const FVector4f TriangleNormal = ((V2.Vertex.WorldPosition - V0.Vertex.WorldPosition) ^ (V1.Vertex.WorldPosition - V0.Vertex.WorldPosition)).GetSafeNormal();
|
|
|
|
// Don't rasterize degenerates
|
|
if (!TriangleNormal.IsNearlyZero3())
|
|
{
|
|
const FVector2f UV0 = V0.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->SurfaceCacheSizeX,TextureMapping->SurfaceCacheSizeY);
|
|
const FVector2f UV1 = V1.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->SurfaceCacheSizeX,TextureMapping->SurfaceCacheSizeY);
|
|
const FVector2f UV2 = V2.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->SurfaceCacheSizeX,TextureMapping->SurfaceCacheSizeY);
|
|
|
|
// Odd number of samples so that the center of the pyramid is on one of the samples
|
|
const uint32 NumSamplesX = 5;
|
|
const uint32 NumSamplesY = 5;
|
|
|
|
// Rasterize multiple sub-texel samples and linearly combine the results
|
|
// Don't rasterize the first or last row and column as the weight will be 0
|
|
for(int32 Y = 1; Y < NumSamplesY - 1; Y++)
|
|
{
|
|
const float SampleYOffset = -Y / (float)(NumSamplesY - 1);
|
|
for(int32 X = 1; X < NumSamplesX - 1; X++)
|
|
{
|
|
const float SampleXOffset = -X / (float)(NumSamplesX - 1);
|
|
// Weight the sample based on a pyramid centered on the texel.
|
|
// The sample with the maximum weight is used, which will be the center if it lies on a triangle.
|
|
const float SampleWeight = (1 - FMath::Abs(1 + SampleXOffset * 2)) * (1 - FMath::Abs(1 + SampleYOffset * 2));
|
|
checkSlow(SampleWeight > 0);
|
|
// Rasterize the triangle using the mapping's texture coordinate channel.
|
|
FTriangleRasterizer<FStaticLightingRasterPolicy> TexelMappingRasterizer(FStaticLightingRasterPolicy(
|
|
Scene,
|
|
TexelToVertexMap,
|
|
SampleWeight,
|
|
TriangleNormal,
|
|
bDebugThisMapping,
|
|
GeneralSettings.bUseMaxWeight
|
|
));
|
|
|
|
TexelMappingRasterizer.DrawTriangle(
|
|
V0,
|
|
V1,
|
|
V2,
|
|
UV0 + FVector2f(SampleXOffset, SampleYOffset),
|
|
UV1 + FVector2f(SampleXOffset, SampleYOffset),
|
|
UV2 + FVector2f(SampleXOffset, SampleYOffset),
|
|
false
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AdjustRepresentativeSurfelForTexelsTextureMapping(TextureMapping, TexelToVertexMap, TexelToCornersMap, nullptr, MappingContext, bDebugThisMapping);
|
|
}
|
|
|
|
void FStaticLightingSystem::AdjustRepresentativeSurfelForTexelsTextureMapping(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FTexelToVertexMap& TexelToVertexMap,
|
|
FTexelToCornersMap& TexelToCornersMap,
|
|
FGatheredLightMapData2D* LightMapData,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
bool bDebugThisMapping) const
|
|
{
|
|
// Iterate over each texel and normalize vectors, calculate texel radius
|
|
for (int32 Y = 0; Y < TexelToVertexMap.GetSizeY(); Y++)
|
|
{
|
|
for (int32 X = 0; X < TexelToVertexMap.GetSizeX(); X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
bool bFoundValidCorner = false;
|
|
const FTexelToCorners& TexelToCorners = TexelToCornersMap(X, Y);
|
|
for (int32 CornerIndex = 0; CornerIndex < NumTexelCorners; CornerIndex++)
|
|
{
|
|
bFoundValidCorner = bFoundValidCorner || TexelToCorners.bValid[CornerIndex];
|
|
}
|
|
|
|
FTexelToVertex& TexelToVertex = TexelToVertexMap(X, Y);
|
|
if (TexelToVertex.TotalSampleWeight > 0.0f || bFoundValidCorner)
|
|
{
|
|
// Use a corner if none of the other samples were valid
|
|
if (TexelToVertex.TotalSampleWeight < DELTA)
|
|
{
|
|
for (int32 CornerIndex = 0; CornerIndex < NumTexelCorners; CornerIndex++)
|
|
{
|
|
if (TexelToCorners.bValid[CornerIndex])
|
|
{
|
|
TexelToVertex.TotalSampleWeight = 1.0f;
|
|
TexelToVertex.WorldPosition = TexelToCorners.Corners[CornerIndex].WorldPosition;
|
|
TexelToVertex.WorldTangentX = TexelToCorners.WorldTangentX;
|
|
TexelToVertex.WorldTangentY = TexelToCorners.WorldTangentY;
|
|
TexelToVertex.WorldTangentZ = TexelToCorners.WorldTangentZ;
|
|
TexelToVertex.TriangleNormal = TexelToCorners.WorldTangentZ;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (GeneralSettings.bUseMaxWeight)
|
|
{
|
|
// Weighted average
|
|
TexelToVertex.WorldTangentX = TexelToVertex.WorldTangentX / TexelToVertex.TotalSampleWeight;
|
|
TexelToVertex.WorldTangentY = TexelToVertex.WorldTangentY / TexelToVertex.TotalSampleWeight;
|
|
TexelToVertex.WorldTangentZ = TexelToVertex.WorldTangentZ / TexelToVertex.TotalSampleWeight;
|
|
TexelToVertex.TriangleNormal = TexelToVertex.TriangleNormal / TexelToVertex.TotalSampleWeight;
|
|
|
|
// Weighted average of opposing vectors can result in a zero vector, fixup with corner
|
|
if (bFoundValidCorner
|
|
&& (TexelToVertex.WorldTangentX.SizeSquared3() < KINDA_SMALL_NUMBER
|
|
|| TexelToVertex.WorldTangentZ.SizeSquared3() < KINDA_SMALL_NUMBER
|
|
|| TexelToVertex.TriangleNormal.SizeSquared3() < KINDA_SMALL_NUMBER))
|
|
{
|
|
for (int32 CornerIndex = 0; CornerIndex < NumTexelCorners; CornerIndex++)
|
|
{
|
|
if (TexelToCorners.bValid[CornerIndex])
|
|
{
|
|
TexelToVertex.WorldTangentX = TexelToCorners.WorldTangentX;
|
|
TexelToVertex.WorldTangentY = TexelToCorners.WorldTangentY;
|
|
TexelToVertex.WorldTangentZ = TexelToCorners.WorldTangentZ;
|
|
TexelToVertex.TriangleNormal = TexelToCorners.WorldTangentZ;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (LightMapData != nullptr)
|
|
{
|
|
// Mark the texel as mapped to some geometry in the scene
|
|
FGatheredLightMapSample& CurrentLightSample = (*LightMapData)(X, Y);
|
|
CurrentLightSample.bIsMapped = true;
|
|
}
|
|
|
|
if (MaterialSettings.bUseNormalMapsForLighting && TextureMapping->Mesh->HasImportedNormal(TexelToVertex.ElementIndex))
|
|
{
|
|
const FVector4f TangentNormal = TextureMapping->Mesh->EvaluateNormal(TexelToVertex.TextureCoordinates[0], TexelToVertex.ElementIndex);
|
|
|
|
const FVector4f WorldTangentRow0(TexelToVertex.WorldTangentX.X, TexelToVertex.WorldTangentY.X, TexelToVertex.WorldTangentZ.X);
|
|
const FVector4f WorldTangentRow1(TexelToVertex.WorldTangentX.Y, TexelToVertex.WorldTangentY.Y, TexelToVertex.WorldTangentZ.Y);
|
|
const FVector4f WorldTangentRow2(TexelToVertex.WorldTangentX.Z, TexelToVertex.WorldTangentY.Z, TexelToVertex.WorldTangentZ.Z);
|
|
const FVector4f WorldVector(
|
|
Dot3(WorldTangentRow0, TangentNormal),
|
|
Dot3(WorldTangentRow1, TangentNormal),
|
|
Dot3(WorldTangentRow2, TangentNormal)
|
|
);
|
|
|
|
TexelToVertex.WorldTangentZ = WorldVector;
|
|
}
|
|
|
|
// Normalize the tangent basis and ensure it is orthonormal
|
|
TexelToVertex.WorldTangentZ = TexelToVertex.WorldTangentZ.GetUnsafeNormal3();
|
|
|
|
const bool bUseVertexNormalForHemisphereGather = TextureMapping->Mesh->UseVertexNormalForHemisphereGather(TexelToVertex.ElementIndex);
|
|
TexelToVertex.TriangleNormal = bUseVertexNormalForHemisphereGather ? TexelToVertex.WorldTangentZ : TexelToVertex.TriangleNormal.GetUnsafeNormal3();
|
|
checkSlow(!TexelToVertex.TriangleNormal.ContainsNaN());
|
|
|
|
const FVector4f OriginalTangentX = TexelToVertex.WorldTangentX;
|
|
const FVector4f OriginalTangentY = TexelToVertex.WorldTangentY;
|
|
|
|
TexelToVertex.WorldTangentY = (TexelToVertex.WorldTangentZ ^ TexelToVertex.WorldTangentX).GetUnsafeNormal3();
|
|
// Maintain handedness
|
|
if (Dot3(TexelToVertex.WorldTangentY, OriginalTangentY) < 0)
|
|
{
|
|
TexelToVertex.WorldTangentY *= -1.0f;
|
|
}
|
|
TexelToVertex.WorldTangentX = TexelToVertex.WorldTangentY ^ TexelToVertex.WorldTangentZ;
|
|
if (Dot3(TexelToVertex.WorldTangentX, OriginalTangentX) < 0)
|
|
{
|
|
TexelToVertex.WorldTangentX *= -1.0f;
|
|
}
|
|
checkSlow(TexelToVertex.WorldTangentX.IsUnit3());
|
|
checkSlow(TexelToVertex.WorldTangentY.IsUnit3());
|
|
checkSlow(TexelToVertex.WorldTangentZ.IsUnit3());
|
|
checkSlow(TexelToVertex.TriangleNormal.IsUnit3());
|
|
checkSlow(Dot3(TexelToVertex.WorldTangentZ, TexelToVertex.WorldTangentY) < KINDA_SMALL_NUMBER);
|
|
checkSlow(Dot3(TexelToVertex.WorldTangentX, TexelToVertex.WorldTangentY) < KINDA_SMALL_NUMBER);
|
|
checkSlow(Dot3(TexelToVertex.WorldTangentX, TexelToVertex.WorldTangentZ) < KINDA_SMALL_NUMBER);
|
|
|
|
// Calculate the bounding radius of the texel
|
|
// Use the closest corner as it's likely that's on the same section of a split texel
|
|
// (A texel shared by multiple UV charts that has sub samples on triangles in different smoothing groups)
|
|
float MinDistanceSquared = FLT_MAX;
|
|
if (bFoundValidCorner)
|
|
{
|
|
for (int32 CornerIndex = 0; CornerIndex < NumTexelCorners; CornerIndex++)
|
|
{
|
|
if (TexelToCorners.bValid[CornerIndex])
|
|
{
|
|
const float CornerDistSquared = (TexelToCorners.Corners[CornerIndex].WorldPosition - TexelToVertex.WorldPosition).SizeSquared3();
|
|
if (CornerDistSquared < MinDistanceSquared)
|
|
{
|
|
MinDistanceSquared = CornerDistSquared;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MinDistanceSquared = SceneConstants.SmallestTexelRadius;
|
|
}
|
|
TexelToVertex.TexelRadius = FMath::Max(FMath::Sqrt(MinDistanceSquared), SceneConstants.SmallestTexelRadius);
|
|
TexelToVertex.SampleRadius = TexelToVertex.TexelRadius;
|
|
MappingContext.Stats.NumMappedTexels++;
|
|
|
|
{
|
|
const FFullStaticLightingVertex FullVertex = TexelToVertex.GetFullVertex();
|
|
const FVector4f TexelCenterOffset = FullVertex.WorldPosition + FullVertex.TriangleNormal * TexelToVertex.TexelRadius * SceneConstants.VisibilityNormalOffsetSampleRadiusScale;
|
|
|
|
FLightRayIntersection Intersections[4];
|
|
bool bHitBackfaces[4];
|
|
|
|
FVector2f CornerSigns[4];
|
|
CornerSigns[0] = FVector2f(1, 1);
|
|
CornerSigns[1] = FVector2f(-1, 1);
|
|
CornerSigns[2] = FVector2f(1, -1);
|
|
CornerSigns[3] = FVector2f(-1, -1);
|
|
|
|
float BackfaceDetectionDistance = TexelToVertex.TexelRadius * 2.5f;
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < UE_ARRAY_COUNT(CornerSigns); CornerIndex++)
|
|
{
|
|
TraceToTexelCorner(
|
|
TexelCenterOffset,
|
|
FullVertex,
|
|
CornerSigns[CornerIndex],
|
|
// Note: Searching the entire influence of the texel after interpolation, which is 2x the sample radius
|
|
BackfaceDetectionDistance,
|
|
MappingContext,
|
|
Intersections[CornerIndex],
|
|
bHitBackfaces[CornerIndex],
|
|
bDebugThisTexel);
|
|
}
|
|
|
|
int32 ClosestIntersectionIndex = INDEX_NONE;
|
|
float ClosestIntersectionDistanceSq = FLT_MAX;
|
|
|
|
int32 ClosestBackfacingIntersectionIndex = INDEX_NONE;
|
|
// Limit the distance that we will search for an intersecting backface in order to move the shading position to the texel radius
|
|
float ClosestBackfacingIntersectionDistanceSq = BackfaceDetectionDistance * BackfaceDetectionDistance;
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < UE_ARRAY_COUNT(CornerSigns); CornerIndex++)
|
|
{
|
|
if (Intersections[CornerIndex].bIntersects)
|
|
{
|
|
const float DistanceSquared = (Intersections[CornerIndex].IntersectionVertex.WorldPosition - TexelCenterOffset).SizeSquared3();
|
|
|
|
if (!bHitBackfaces[CornerIndex] && (ClosestIntersectionIndex == INDEX_NONE || DistanceSquared < ClosestIntersectionDistanceSq))
|
|
{
|
|
ClosestIntersectionDistanceSq = DistanceSquared;
|
|
ClosestIntersectionIndex = CornerIndex;
|
|
|
|
// Mark the texel as intersecting another surface so we can avoid filtering across it later
|
|
TexelToVertex.bIntersectingSurface = true;
|
|
}
|
|
|
|
if (bHitBackfaces[CornerIndex] && (DistanceSquared < ClosestBackfacingIntersectionDistanceSq && !FMath::IsNearlyEqual(DistanceSquared, ClosestBackfacingIntersectionDistanceSq, SceneConstants.SmallestTexelRadius)))
|
|
{
|
|
ClosestBackfacingIntersectionDistanceSq = DistanceSquared;
|
|
ClosestBackfacingIntersectionIndex = CornerIndex;
|
|
|
|
// Mark the texel as intersecting another surface so we can avoid filtering across it later
|
|
TexelToVertex.bIntersectingSurface = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ClosestIntersectionIndex != INDEX_NONE)
|
|
{
|
|
checkSlow(Intersections[ClosestIntersectionIndex].bIntersects);
|
|
|
|
TexelToVertex.SampleRadius = FMath::Min(TexelToVertex.TexelRadius, FMath::Sqrt(ClosestIntersectionDistanceSq / 2.0f));
|
|
}
|
|
|
|
// Give preference to moving the shading position outside of backfaces
|
|
int32 IntersectionIndexForShadingPositionMovement = ClosestBackfacingIntersectionIndex;
|
|
|
|
// Note: this is disabled as it causes problems in cracks, the lighting position will be moved inside the object
|
|
/*
|
|
// Even if we didn't hit any backfaces, still move the shading position away from an intersecting frontface if it is close enough
|
|
if (IntersectionIndexForShadingPositionMovement == INDEX_NONE
|
|
&& ClosestIntersectionDistanceSq < (TexelToVertex.TexelRadius / 2) * (TexelToVertex.TexelRadius / 2))
|
|
{
|
|
IntersectionIndexForShadingPositionMovement = ClosestIntersectionIndex;
|
|
}*/
|
|
|
|
if (IntersectionIndexForShadingPositionMovement != INDEX_NONE)
|
|
{
|
|
// Move the shading position outside the surface that is intersecting this texel
|
|
const FVector4f OffsetShadingPosition = Intersections[IntersectionIndexForShadingPositionMovement].IntersectionVertex.WorldPosition
|
|
// Move along the intersecting surface's normal but also away from the texel a bit to prevent incorrect self occlusion
|
|
+ (Intersections[IntersectionIndexForShadingPositionMovement].IntersectionVertex.WorldTangentZ + TexelToVertex.TriangleNormal) * 0.5f * TexelToVertex.TexelRadius * SceneConstants.VisibilityNormalOffsetSampleRadiusScale;
|
|
|
|
// Project back onto plane of texel to avoid incorrect self occlusion
|
|
TexelToVertex.WorldPosition = OffsetShadingPosition + TexelToVertex.TriangleNormal * Dot3(TexelToVertex.TriangleNormal, TexelToVertex.WorldPosition - OffsetShadingPosition);
|
|
|
|
TexelToVertex.SampleRadius = (OffsetShadingPosition - Intersections[IntersectionIndexForShadingPositionMovement].IntersectionVertex.WorldPosition).Size3() / FMath::Sqrt(2.0f);
|
|
}
|
|
|
|
TexelToVertex.SampleRadius = FMath::Max(TexelToVertex.SampleRadius, SceneConstants.SmallestTexelRadius);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (LightMapData != nullptr)
|
|
{
|
|
// Mark unmapped texels with the supplied 'UnmappedTexelColor'.
|
|
FGatheredLightMapSample& CurrentLightSample = (*LightMapData)(X, Y);
|
|
CurrentLightSample.AddWeighted(FGatheredLightSampleUtil::AmbientLight<2>(Scene.GeneralSettings.UnmappedTexelColor), 1.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FStaticLightingSystem::FinalizeSurfaceCacheTextureMapping(FStaticLightingTextureMapping* TextureMapping)
|
|
{
|
|
#if LIGHTMASS_DO_PROCESSING
|
|
checkSlow(TextureMapping);
|
|
FStaticLightingMappingContext MappingContext(TextureMapping->Mesh,*this,&StartupDebugOutput);
|
|
const FBoxSphereBounds3f ImportanceBounds = Scene.GetImportanceBounds();
|
|
|
|
FTexelToVertexMap TexelToVertexMap(TextureMapping->SurfaceCacheSizeX, TextureMapping->SurfaceCacheSizeY);
|
|
|
|
bool bDebugThisMapping = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
bDebugThisMapping = TextureMapping == Scene.DebugMapping;
|
|
int32 CacheDebugX = -1;
|
|
int32 CacheDebugY = -1;
|
|
if (bDebugThisMapping)
|
|
{
|
|
CacheDebugX = FMath::TruncToInt(Scene.DebugInput.LocalX / (float)TextureMapping->CachedSizeX * TextureMapping->SurfaceCacheSizeX);
|
|
CacheDebugY = FMath::TruncToInt(Scene.DebugInput.LocalY / (float)TextureMapping->CachedSizeY * TextureMapping->SurfaceCacheSizeY);
|
|
}
|
|
#endif
|
|
|
|
RasterizeToSurfaceCacheTextureMapping(TextureMapping, bDebugThisMapping, TexelToVertexMap);
|
|
|
|
for (int32 Y = 0; Y < TextureMapping->SurfaceCacheSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->SurfaceCacheSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == CacheDebugX
|
|
&& X == CacheDebugY)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
|
|
if (TexelToVertex.TotalSampleWeight > 0.0f)
|
|
{
|
|
FFullStaticLightingVertex CurrentVertex = TexelToVertex.GetFullVertex();
|
|
CurrentVertex.ApplyVertexModifications(TexelToVertex.ElementIndex, MaterialSettings.bUseNormalMapsForLighting, TextureMapping->Mesh);
|
|
|
|
const int32 SurfaceCacheIndex = Y * TextureMapping->SurfaceCacheSizeX + X;
|
|
|
|
FLinearColor FinalIncidentLighting = FLinearColor::Black;
|
|
|
|
// SurfaceCacheLighting at this point contains 1st and up bounce lighting for the skylight and emissive sources, computed by the radiosity iterations
|
|
FinalIncidentLighting += TextureMapping->SurfaceCacheLighting[SurfaceCacheIndex];
|
|
|
|
if ((GeneralSettings.ViewSingleBounceNumber < 0 || GeneralSettings.ViewSingleBounceNumber >= 2)
|
|
&& !ImportanceTracingSettings.bUseRadiositySolverForLightMultibounce
|
|
&& PhotonMappingSettings.bUseIrradiancePhotons)
|
|
{
|
|
const FIrradiancePhoton* NearestPhoton = TextureMapping->CachedIrradiancePhotons[SurfaceCacheIndex];
|
|
|
|
if (NearestPhoton)
|
|
{
|
|
// The irradiance photon contains 2nd and up bounce lighting for point / spot / directional lights (since they emit photons)
|
|
FinalIncidentLighting += NearestPhoton->GetIrradiance();
|
|
}
|
|
}
|
|
|
|
if (GeneralSettings.ViewSingleBounceNumber < 0 || GeneralSettings.ViewSingleBounceNumber == 1)
|
|
{
|
|
FGatheredLightSample DirectLighting;
|
|
float Unused2;
|
|
TArray<FVector3f, TInlineAllocator<1>> VertexOffsets;
|
|
VertexOffsets.Add(FVector3f(0, 0, 0));
|
|
|
|
CalculateApproximateDirectLighting(CurrentVertex, TexelToVertex.TexelRadius, VertexOffsets, .1f, true, true, bDebugThisTexel && PhotonMappingSettings.bVisualizeCachedApproximateDirectLighting, MappingContext, DirectLighting, Unused2);
|
|
FinalIncidentLighting += DirectLighting.IncidentLighting;
|
|
}
|
|
|
|
const bool bTranslucent = TextureMapping->Mesh->IsTranslucent(TexelToVertex.ElementIndex);
|
|
const FLinearColor Reflectance = (bTranslucent ? FLinearColor::Black : TextureMapping->Mesh->EvaluateTotalReflectance(CurrentVertex, TexelToVertex.ElementIndex)) * (float)INV_PI;
|
|
// Combine all the lighting and surface reflectance so the final gather ray only needs one memory fetch
|
|
TextureMapping->SurfaceCacheLighting[SurfaceCacheIndex] = FinalIncidentLighting * Reflectance;
|
|
}
|
|
}
|
|
}
|
|
|
|
TextureMapping->CachedIrradiancePhotons.Empty();
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Builds lighting for a texture mapping.
|
|
* @param TextureMapping - The mapping to build lighting for.
|
|
*/
|
|
void FStaticLightingSystem::ProcessTextureMapping(FStaticLightingTextureMapping* TextureMapping)
|
|
{
|
|
FPlatformAtomics::InterlockedIncrement(&TasksInProgressThatWillNeedHelp);
|
|
checkSlow(TextureMapping);
|
|
// calculate the total time just for processing
|
|
double StartTime = FPlatformTime::Seconds();
|
|
|
|
bool bDebugThisMapping = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
bDebugThisMapping = TextureMapping == Scene.DebugMapping;
|
|
#endif
|
|
|
|
TList<FTextureMappingStaticLightingData>* StaticLightingLink = new TList<FTextureMappingStaticLightingData>(FTextureMappingStaticLightingData(),NULL);
|
|
|
|
// light guid to shadow map mapping
|
|
TMap<const FLight*, FShadowMapData2D*> ShadowMaps;
|
|
TMap<const FLight*, FSignedDistanceFieldShadowMapData2D*> SignedDistanceFieldShadowMaps;
|
|
|
|
if (bDebugThisMapping)
|
|
{
|
|
StaticLightingLink->Element.DebugOutput = StartupDebugOutput;
|
|
}
|
|
|
|
FStaticLightingMappingContext MappingContext(TextureMapping->Mesh, *this, &StaticLightingLink->Element.DebugOutput);
|
|
|
|
// Allocate light-map data.
|
|
FGatheredLightMapData2D LightMapData(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
|
|
LightMapData.bHasSkyShadowing = HasSkyShadowing();
|
|
|
|
// if we have a debug texel, then only compute the lighting for this mapping
|
|
bool bCalculateThisMapping = true;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
// we want to skip mappings if the setting is enabled, and we have a debug mapping, and it's not this one
|
|
bCalculateThisMapping = !(Scene.bOnlyCalcDebugTexelMappings && Scene.DebugMapping != NULL && !bDebugThisMapping);
|
|
#endif
|
|
|
|
// Allocate the texel to vertex map.
|
|
FTexelToVertexMap TexelToVertexMap(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
|
|
const double TexelRasterizationStart = FPlatformTime::Seconds();
|
|
// Allocate a map from texel to the corners of that texel
|
|
FTexelToCornersMap TexelToCornersMap(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
SetupTextureMapping(TextureMapping, LightMapData, TexelToVertexMap, TexelToCornersMap, MappingContext, bDebugThisMapping);
|
|
MappingContext.Stats.TexelRasterizationTime += FPlatformTime::Seconds() - TexelRasterizationStart;
|
|
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping)
|
|
{
|
|
MappingContext.DebugOutput->bValid = true;
|
|
for (int32 Y = 0;Y < TextureMapping->CachedSizeY;Y++)
|
|
{
|
|
for (int32 X = 0;X < TextureMapping->CachedSizeX;X++)
|
|
{
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
if (TexelToVertex.TotalSampleWeight > 0.0f)
|
|
{
|
|
// Verify that vertex normals are normalized (within some error that is large because of FPackedNormal)
|
|
checkSlow(FVector3f(TexelToVertex.WorldTangentZ).IsUnit(.1f));
|
|
|
|
const float DistanceToDebugTexelSq = FVector3f(TexelToVertex.WorldPosition - Scene.DebugInput.Position).SizeSquared();
|
|
if (DistanceToDebugTexelSq < 40000
|
|
|| X == Scene.DebugInput.LocalX && Y == Scene.DebugInput.LocalY)
|
|
{
|
|
if (X == Scene.DebugInput.LocalX && Y == Scene.DebugInput.LocalY)
|
|
{
|
|
FDebugStaticLightingVertex DebugVertex;
|
|
DebugVertex.VertexNormal = FVector4f(TexelToVertex.WorldTangentZ);
|
|
DebugVertex.VertexPosition = TexelToVertex.WorldPosition;
|
|
MappingContext.DebugOutput->Vertices.Add(DebugVertex);
|
|
|
|
MappingContext.DebugOutput->SelectedVertexIndices.Add(MappingContext.DebugOutput->Vertices.Num() - 1);
|
|
MappingContext.DebugOutput->SampleRadius = TexelToVertex.TexelRadius;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if LIGHTMASS_DO_PROCESSING
|
|
|
|
if (bCalculateThisMapping)
|
|
{
|
|
const double DirectLightingStartTime = FPlatformTime::Seconds();
|
|
const bool bCalculateDirectLightingFromPhotons = PhotonMappingSettings.bUsePhotonMapping && PhotonMappingSettings.bVisualizeCachedApproximateDirectLighting;
|
|
// Only continue if photon mapping will not be used for direct lighting
|
|
if (!bCalculateDirectLightingFromPhotons)
|
|
{
|
|
// Iterate over each light that is relevant to the direct lighting of the mesh
|
|
for (int32 LightIndex = 0; LightIndex < TextureMapping->Mesh->RelevantLights.Num(); LightIndex++)
|
|
{
|
|
const FLight* Light = TextureMapping->Mesh->RelevantLights[LightIndex];
|
|
|
|
// skip sky lights for now
|
|
if (Light->GetSkyLight())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!Light->AffectsBounds(FBoxSphereBounds3f(TextureMapping->Mesh->BoundingBox)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( ShadowSettings.bUseZeroAreaLightmapSpaceFilteredLights)
|
|
{
|
|
// Calculate direct lighting from lights as if they have no area, and then filter in texture space to create approximate penumbras.
|
|
CalculateDirectLightingTextureMappingFiltered(TextureMapping, MappingContext, LightMapData, ShadowMaps, TexelToVertexMap, bDebugThisMapping, Light);
|
|
}
|
|
else
|
|
{
|
|
if (!Light->UseStaticLighting()
|
|
&& (Light->LightFlags & GI_LIGHT_CASTSHADOWS)
|
|
&& (Light->LightFlags & GI_LIGHT_CASTSTATICSHADOWS)
|
|
&& (Light->LightFlags & GI_LIGHT_STORE_SEPARATE_SHADOW_FACTOR)
|
|
&& ShadowSettings.bAllowSignedDistanceFieldShadows)
|
|
{
|
|
if (Light->LightFlags & GI_LIGHT_USE_AREA_SHADOWS_FOR_SEPARATE_SHADOW_FACTOR)
|
|
{
|
|
FShadowMapData2D* ShadowMapData = new FShadowMapData2D(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY);
|
|
CalculateDirectAreaLightingTextureMapping(TextureMapping, MappingContext, LightMapData, ShadowMapData, TexelToVertexMap, bDebugThisMapping, Light);
|
|
|
|
if (ShadowMapData)
|
|
{
|
|
FSignedDistanceFieldShadowMapData2D* ConvertedShadowMapData = new FSignedDistanceFieldShadowMapData2D(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
const FShadowSample& SourceShadowSample = (*ShadowMapData)(X,Y);
|
|
FSignedDistanceFieldShadowSample& DestShadowSample = (*ConvertedShadowMapData)(X,Y);
|
|
DestShadowSample.bIsMapped = SourceShadowSample.bIsMapped;
|
|
// Encode with more precision near 0
|
|
// The decode shader code will undo this in GetPrecomputedShadowMasks
|
|
DestShadowSample.Distance = FMath::Sqrt(FMath::Clamp(SourceShadowSample.Visibility, 0.0f, 1.0f));
|
|
DestShadowSample.PenumbraSize = 1;
|
|
}
|
|
}
|
|
|
|
SignedDistanceFieldShadowMaps.Add(Light, ConvertedShadowMapData);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const bool bUseTextureSpaceDistanceFieldGeneration = true;
|
|
|
|
if (bUseTextureSpaceDistanceFieldGeneration)
|
|
{
|
|
// Calculate distance field shadows, where the distance to the nearest shadow transition is stored instead of just a [0,1] shadow factor.
|
|
CalculateDirectSignedDistanceFieldLightingTextureMappingTextureSpace(TextureMapping, MappingContext, LightMapData, SignedDistanceFieldShadowMaps, TexelToVertexMap, TexelToCornersMap, bDebugThisMapping, Light);
|
|
}
|
|
else
|
|
{
|
|
// Experimental method that avoids artifacts due to lightmap seams
|
|
CalculateDirectSignedDistanceFieldLightingTextureMappingLightSpace(TextureMapping, MappingContext, LightMapData, SignedDistanceFieldShadowMaps, TexelToVertexMap, TexelToCornersMap, bDebugThisMapping, Light);
|
|
}
|
|
}
|
|
}
|
|
else if (Light->UseStaticLighting())
|
|
{
|
|
FShadowMapData2D* ShadowMapData = NULL;
|
|
|
|
// Calculate direct lighting from area lights
|
|
// Shadow penumbras will be correctly shaped and will be softer for larger light sources and distant shadow casters.
|
|
CalculateDirectAreaLightingTextureMapping(TextureMapping, MappingContext, LightMapData, ShadowMapData, TexelToVertexMap, bDebugThisMapping, Light);
|
|
|
|
if (Light->GetMeshAreaLight() == NULL)
|
|
{
|
|
LightMapData.AddLight(Light);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Release corner information as it is no longer needed
|
|
TexelToCornersMap.Empty();
|
|
|
|
// Calculate direct lighting using the direct photon map.
|
|
// This is only useful for debugging what the final gather rays see.
|
|
if (bCalculateDirectLightingFromPhotons)
|
|
{
|
|
CalculateDirectLightingTextureMappingPhotonMap(TextureMapping, MappingContext, LightMapData, ShadowMaps, TexelToVertexMap, bDebugThisMapping);
|
|
}
|
|
MappingContext.Stats.DirectLightingTime += FPlatformTime::Seconds() - DirectLightingStartTime;
|
|
|
|
CalculateIndirectLightingTextureMapping(TextureMapping, MappingContext, LightMapData, TexelToVertexMap, bDebugThisMapping);
|
|
|
|
const double ErrorAndMaterialColoringStart = FPlatformTime::Seconds();
|
|
ViewMaterialAttributesTextureMapping(TextureMapping, MappingContext, LightMapData, TexelToVertexMap, bDebugThisMapping);
|
|
ColorInvalidLightmapUVs(TextureMapping, LightMapData, bDebugThisMapping);
|
|
|
|
// Count the time doing material coloring and invalid lightmap UV color toward texel setup for now
|
|
MappingContext.Stats.TexelRasterizationTime += FPlatformTime::Seconds() - ErrorAndMaterialColoringStart;
|
|
}
|
|
#else
|
|
FPlatformAtomics::InterlockedDecrement(&TasksInProgressThatWillNeedHelp);
|
|
#endif
|
|
|
|
const double PaddingStart = FPlatformTime::Seconds();
|
|
|
|
FGatheredLightMapData2D PaddedLightMapData(TextureMapping->SizeX, TextureMapping->SizeY);
|
|
PadTextureMapping(TextureMapping, LightMapData, PaddedLightMapData, ShadowMaps, SignedDistanceFieldShadowMaps);
|
|
LightMapData.Empty();
|
|
|
|
// calculate the total time just for processing
|
|
const double ExecutionTimeForColoring = FPlatformTime::Seconds() - StartTime;
|
|
|
|
if (!bCalculateThisMapping || Scene.bColorBordersGreen || Scene.bColorByExecutionTime || Scene.bUseRandomColors)
|
|
{
|
|
bool bColorNonBorders = Scene.bColorByExecutionTime || Scene.bUseRandomColors;
|
|
|
|
// calculate what color to put in each spot, if overriding
|
|
FLinearColor OverrideColor(0, 0, 0);
|
|
if (Scene.bColorByExecutionTime)
|
|
{
|
|
OverrideColor.R = ExecutionTimeForColoring / (Scene.ExecutionTimeDivisor ? Scene.ExecutionTimeDivisor : 15.0f);
|
|
}
|
|
else if (Scene.bUseRandomColors)
|
|
{
|
|
// make each mapping solid, random colors
|
|
static FLMRandomStream RandomStream(0);
|
|
|
|
// make a random color
|
|
OverrideColor.R = RandomStream.GetFraction();
|
|
OverrideColor.G = RandomStream.GetFraction();
|
|
OverrideColor.B = RandomStream.GetFraction();
|
|
|
|
if (Scene.bColorBordersGreen)
|
|
{
|
|
// not too green tho so borders show up
|
|
OverrideColor.G /= 2.0f;
|
|
}
|
|
}
|
|
else if (!bCalculateThisMapping)
|
|
{
|
|
OverrideColor = FLinearColor::White;
|
|
}
|
|
|
|
FLinearColor Green(0, 1.0, 0);
|
|
|
|
for (uint32 Y = 0; Y < PaddedLightMapData.GetSizeY(); Y++)
|
|
{
|
|
for (uint32 X = 0; X < PaddedLightMapData.GetSizeX(); X++)
|
|
{
|
|
FGatheredLightMapSample& Sample = PaddedLightMapData(X, Y);
|
|
bool bIsBorder = (X <= 1 || Y <= 1 || X >= PaddedLightMapData.GetSizeX() - 2 || Y >= PaddedLightMapData.GetSizeY() - 2);
|
|
if (!bCalculateThisMapping || (Sample.bIsMapped && bColorNonBorders) || (bIsBorder && Scene.bColorBordersGreen))
|
|
{
|
|
FLinearColor& SampleColor = (bIsBorder && Scene.bColorBordersGreen) ? Green : OverrideColor;
|
|
|
|
Sample.HighQuality.AddWeighted(FGatheredLightSampleUtil::AmbientLight<2>(SampleColor), 1.0f);
|
|
Sample.LowQuality.AddWeighted(FGatheredLightSampleUtil::AmbientLight<2>(SampleColor), 1.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const int32 PaddedDebugX = TextureMapping->bPadded ? Scene.DebugInput.LocalX + 1 : Scene.DebugInput.LocalX;
|
|
const int32 PaddedDebugY = TextureMapping->bPadded ? Scene.DebugInput.LocalY + 1 : Scene.DebugInput.LocalY;
|
|
FLightMapData2D* FinalLightmapData = PaddedLightMapData.ConvertToLightmap2D(bDebugThisMapping, PaddedDebugX, PaddedDebugY);
|
|
|
|
// Count the time doing padding and lightmap coloring toward texel setup
|
|
const double CurrentTime = FPlatformTime::Seconds();
|
|
MappingContext.Stats.TexelRasterizationTime += CurrentTime - PaddingStart;
|
|
const double ExecutionTime = CurrentTime - StartTime;
|
|
|
|
// Enqueue the static lighting for application in the main thread.
|
|
StaticLightingLink->Element.Mapping = TextureMapping;
|
|
StaticLightingLink->Element.LightMapData = FinalLightmapData;
|
|
StaticLightingLink->Element.ShadowMaps = ShadowMaps;
|
|
StaticLightingLink->Element.SignedDistanceFieldShadowMaps = SignedDistanceFieldShadowMaps;
|
|
StaticLightingLink->Element.ExecutionTime = ExecutionTime;
|
|
MappingContext.Stats.TotalTextureMappingLightingThreadTime = ExecutionTime;
|
|
|
|
const int32 PaddedOffset = TextureMapping->bPadded ? 1 : 0;
|
|
const int32 DebugSampleIndex = (Scene.DebugInput.LocalY + PaddedOffset) * TextureMapping->SizeX + Scene.DebugInput.LocalX + PaddedOffset;
|
|
|
|
CompleteTextureMappingList.AddElement(StaticLightingLink);
|
|
|
|
const int32 OldNumTexelsCompleted = FPlatformAtomics::InterlockedAdd(&NumTexelsCompleted, TextureMapping->CachedSizeX * TextureMapping->CachedSizeY);
|
|
UpdateInternalStatus(OldNumTexelsCompleted);
|
|
}
|
|
|
|
class FTexelCornerRasterPolicy
|
|
{
|
|
public:
|
|
|
|
typedef FStaticLightingVertex InterpolantType;
|
|
|
|
/** Initialization constructor. */
|
|
FTexelCornerRasterPolicy(
|
|
const FScene& InScene,
|
|
FTexelToCornersMap& InTexelToCornersMap,
|
|
int32 InCornerIndex,
|
|
bool bInDebugThisMapping
|
|
):
|
|
Scene(InScene),
|
|
TexelToCornersMap(InTexelToCornersMap),
|
|
CornerIndex(InCornerIndex),
|
|
bDebugThisMapping(bInDebugThisMapping)
|
|
{
|
|
}
|
|
|
|
protected:
|
|
|
|
// FTriangleRasterizer policy interface.
|
|
|
|
int32 GetMinX() const { return 0; }
|
|
int32 GetMaxX() const { return TexelToCornersMap.GetSizeX() - 1; }
|
|
int32 GetMinY() const { return 0; }
|
|
int32 GetMaxY() const { return TexelToCornersMap.GetSizeY() - 1; }
|
|
|
|
void ProcessPixel(int32 X, int32 Y, const InterpolantType& Interpolant, bool BackFacing);
|
|
|
|
private:
|
|
|
|
const FScene& Scene;
|
|
|
|
/** The texel to vertex map which is being rasterized to. */
|
|
FTexelToCornersMap& TexelToCornersMap;
|
|
|
|
/** Index of the current corner being rasterized */
|
|
const int32 CornerIndex;
|
|
|
|
const bool bDebugThisMapping;
|
|
};
|
|
|
|
void FTexelCornerRasterPolicy::ProcessPixel(int32 X, int32 Y, const InterpolantType& Vertex, bool BackFacing)
|
|
{
|
|
FTexelToCorners& TexelToCorners = TexelToCornersMap(X, Y);
|
|
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& X == Scene.DebugInput.LocalX
|
|
&& Y == Scene.DebugInput.LocalY)
|
|
{
|
|
int32 TempBreak = 0;
|
|
}
|
|
#endif
|
|
|
|
TexelToCorners.Corners[CornerIndex].WorldPosition = Vertex.WorldPosition;
|
|
TexelToCorners.WorldTangentX = Vertex.WorldTangentX;
|
|
TexelToCorners.WorldTangentY = Vertex.WorldTangentY;
|
|
TexelToCorners.WorldTangentZ = Vertex.WorldTangentZ;
|
|
TexelToCorners.bValid[CornerIndex] = true;
|
|
}
|
|
|
|
void FStaticLightingSystem::TraceToTexelCorner(
|
|
const FVector4f& TexelCenterOffset,
|
|
const FFullStaticLightingVertex& FullVertex,
|
|
FVector2f CornerSigns,
|
|
float TexelRadius,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FLightRayIntersection& Intersection,
|
|
bool& bHitBackface,
|
|
bool bDebugThisTexel) const
|
|
{
|
|
// Vector from the center to one of the corners of the texel
|
|
// The FMath::Sqrt(.5f) is to normalize (Vertex.TriangleTangentX + Vertex.TriangleTangentY), which are orthogonal unit vectors.
|
|
const FVector4f CornerOffset = FMath::Sqrt(.5f) * (CornerSigns.X * FullVertex.TriangleTangentX + CornerSigns.Y * FullVertex.TriangleTangentY) * TexelRadius * SceneConstants.VisibilityTangentOffsetSampleRadiusScale;
|
|
const FLightRay TexelRay(
|
|
TexelCenterOffset,
|
|
TexelCenterOffset + CornerOffset,
|
|
NULL,
|
|
NULL
|
|
);
|
|
|
|
AggregateMesh->IntersectLightRay(TexelRay, true, false, false, MappingContext.RayCache, Intersection);
|
|
|
|
bHitBackface = Intersection.bIntersects && Dot3(Intersection.IntersectionVertex.WorldTangentZ, TexelRay.Direction) >= 0 && !Intersection.Mesh->IsTwoSided(Intersection.ElementIndex);
|
|
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisTexel)
|
|
{
|
|
FDebugStaticLightingRay DebugRay(TexelRay.Start, TexelRay.End, Intersection.bIntersects);
|
|
if (Intersection.bIntersects)
|
|
{
|
|
DebugRay.End = Intersection.IntersectionVertex.WorldPosition;
|
|
}
|
|
MappingContext.DebugOutput->ShadowRays.Add(DebugRay);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/** Calculates TexelToVertexMap and initializes each texel's light sample as mapped or not. */
|
|
void FStaticLightingSystem::SetupTextureMapping(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
FTexelToVertexMap& TexelToVertexMap,
|
|
FTexelToCornersMap& TexelToCornersMap,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
bool bDebugThisMapping) const
|
|
{
|
|
CalculateTexelCorners(TextureMapping->Mesh, TexelToCornersMap, TextureMapping->LightmapTextureCoordinateIndex, bDebugThisMapping);
|
|
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping)
|
|
{
|
|
const FTexelToCorners& TexelToCorners = TexelToCornersMap(Scene.DebugInput.LocalX, Scene.DebugInput.LocalY);
|
|
for (int32 CornerIndex = 0; CornerIndex < NumTexelCorners; CornerIndex++)
|
|
{
|
|
MappingContext.DebugOutput->TexelCorners[CornerIndex] = TexelToCorners.Corners[CornerIndex].WorldPosition;
|
|
MappingContext.DebugOutput->bCornerValid[CornerIndex] = TexelToCorners.bValid[CornerIndex] != 0;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Rasterize the triangles into the texel to vertex map.
|
|
if (GeneralSettings.bUseConservativeTexelRasterization && TextureMapping->bBilinearFilter == true)
|
|
{
|
|
// Using conservative rasterization, which uses super sampling to try to detect all texels that should be mapped.
|
|
for(int32 TriangleIndex = 0;TriangleIndex < TextureMapping->Mesh->NumTriangles;TriangleIndex++)
|
|
{
|
|
// Query the mesh for the triangle's vertices.
|
|
FStaticLightingInterpolant V0;
|
|
FStaticLightingInterpolant V1;
|
|
FStaticLightingInterpolant V2;
|
|
int32 Element;
|
|
TextureMapping->Mesh->GetTriangle(TriangleIndex,V0.Vertex,V1.Vertex,V2.Vertex,Element);
|
|
V0.ElementIndex = V1.ElementIndex = V2.ElementIndex = Element;
|
|
|
|
const FVector4f TriangleNormal = ((V2.Vertex.WorldPosition - V0.Vertex.WorldPosition) ^ (V1.Vertex.WorldPosition - V0.Vertex.WorldPosition)).GetSafeNormal();
|
|
|
|
// Don't rasterize degenerates
|
|
if (!TriangleNormal.IsNearlyZero3())
|
|
{
|
|
const FVector2f UV0 = V0.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY);
|
|
const FVector2f UV1 = V1.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY);
|
|
const FVector2f UV2 = V2.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY);
|
|
|
|
// Odd number of samples so that the center of the pyramid is on one of the samples
|
|
const uint32 NumSamplesX = 7;
|
|
const uint32 NumSamplesY = 7;
|
|
|
|
// Rasterize multiple sub-texel samples and linearly combine the results
|
|
// Don't rasterize the first or last row and column as the weight will be 0
|
|
for(int32 Y = 1; Y < NumSamplesY - 1; Y++)
|
|
{
|
|
const float SampleYOffset = -Y / (float)(NumSamplesY - 1);
|
|
for(int32 X = 1; X < NumSamplesX - 1; X++)
|
|
{
|
|
const float SampleXOffset = -X / (float)(NumSamplesX - 1);
|
|
// Weight the sample based on a pyramid centered on the texel.
|
|
// The sample with the maximum weight is used, which will be the center if it lies on a triangle.
|
|
const float SampleWeight = (1 - FMath::Abs(1 + SampleXOffset * 2)) * (1 - FMath::Abs(1 + SampleYOffset * 2));
|
|
checkSlow(SampleWeight > 0);
|
|
// Rasterize the triangle using the mapping's texture coordinate channel.
|
|
FTriangleRasterizer<FStaticLightingRasterPolicy> TexelMappingRasterizer(FStaticLightingRasterPolicy(
|
|
Scene,
|
|
TexelToVertexMap,
|
|
SampleWeight,
|
|
TriangleNormal,
|
|
bDebugThisMapping,
|
|
GeneralSettings.bUseMaxWeight
|
|
));
|
|
|
|
TexelMappingRasterizer.DrawTriangle(
|
|
V0,
|
|
V1,
|
|
V2,
|
|
UV0 + FVector2f(SampleXOffset, SampleYOffset),
|
|
UV1 + FVector2f(SampleXOffset, SampleYOffset),
|
|
UV2 + FVector2f(SampleXOffset, SampleYOffset),
|
|
false
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Only rasterizing one sample at the texel's center. If the center does not lie on a triangle, the texel will not be mapped.
|
|
const float SampleWeight = 1.0f;
|
|
// Rasterize the triangles offset by the random sample location.
|
|
for(int32 TriangleIndex = 0;TriangleIndex < TextureMapping->Mesh->NumTriangles;TriangleIndex++)
|
|
{
|
|
// Query the mesh for the triangle's vertices.
|
|
FStaticLightingInterpolant V0;
|
|
FStaticLightingInterpolant V1;
|
|
FStaticLightingInterpolant V2;
|
|
int32 Element;
|
|
TextureMapping->Mesh->GetTriangle(TriangleIndex,V0.Vertex,V1.Vertex,V2.Vertex,Element);
|
|
V0.ElementIndex = V1.ElementIndex = V2.ElementIndex = Element;
|
|
|
|
// Rasterize the triangle using the mapping's texture coordinate channel.
|
|
FTriangleRasterizer<FStaticLightingRasterPolicy> TexelMappingRasterizer(FStaticLightingRasterPolicy(
|
|
Scene,
|
|
TexelToVertexMap,
|
|
SampleWeight,
|
|
FVector4f(0),
|
|
bDebugThisMapping,
|
|
false
|
|
));
|
|
|
|
// Only rasterize the center of the texel, any texel whose center does not lie on a triangle will not be mapped.
|
|
TexelMappingRasterizer.DrawTriangle(
|
|
V0,
|
|
V1,
|
|
V2,
|
|
V0.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY) + FVector2f(-0.5f,-0.5f),
|
|
V1.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY) + FVector2f(-0.5f,-0.5f),
|
|
V2.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY) + FVector2f(-0.5f,-0.5f),
|
|
false
|
|
);
|
|
}
|
|
}
|
|
|
|
AdjustRepresentativeSurfelForTexelsTextureMapping(TextureMapping, TexelToVertexMap, TexelToCornersMap, &LightMapData, MappingContext, bDebugThisMapping);
|
|
}
|
|
|
|
/** Calculates direct lighting as if all lights were non-area lights, then filters the results in texture space to create approximate soft shadows. */
|
|
void FStaticLightingSystem::CalculateDirectLightingTextureMappingFiltered(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
TMap<const FLight*, FShadowMapData2D*>& ShadowMaps,
|
|
const FTexelToVertexMap& TexelToVertexMap,
|
|
bool bDebugThisMapping,
|
|
const FLight* Light) const
|
|
{
|
|
// Raytrace the texels of the shadow-map that map to vertices on a world-space surface.
|
|
FShadowMapData2D ShadowMapData(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY);
|
|
for (int32 Y = 0;Y < TextureMapping->CachedSizeY;Y++)
|
|
{
|
|
for (int32 X = 0;X < TextureMapping->CachedSizeX;X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
if (TexelToVertex.TotalSampleWeight > 0.0f)
|
|
{
|
|
FShadowSample& ShadowSample = ShadowMapData(X,Y);
|
|
ShadowSample.bIsMapped = true;
|
|
|
|
// Check if the light is in front of the surface.
|
|
const bool bLightIsInFrontOfTriangle = !IsLightBehindSurface(TexelToVertex.WorldPosition,FVector4f(TexelToVertex.WorldTangentZ),Light);
|
|
if (bLightIsInFrontOfTriangle || TextureMapping->Mesh->IsTwoSided(TexelToVertex.ElementIndex))
|
|
{
|
|
// Compute the shadow factors for this sample from the shadow-mapped lights.
|
|
ShadowSample.Visibility = CalculatePointShadowing(TextureMapping,TexelToVertex.WorldPosition,Light,MappingContext,bDebugThisTexel)
|
|
? 0.0f : 1.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter the shadow-map, and detect completely occluded lights.
|
|
FShadowMapData2D* FilteredShadowMapData = new FShadowMapData2D(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY);;
|
|
bool bIsCompletelyOccluded = true;
|
|
for (int32 Y = 0;Y < TextureMapping->CachedSizeY;Y++)
|
|
{
|
|
for (int32 X = 0;X < TextureMapping->CachedSizeX;X++)
|
|
{
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
int32 TempBreak = 0;
|
|
}
|
|
#endif
|
|
if (ShadowMapData(X,Y).bIsMapped)
|
|
{
|
|
uint32 Visibility = 0;
|
|
uint32 Coverage = 0;
|
|
// The shadow-map filter.
|
|
static const uint32 FilterSizeX = 5;
|
|
static const uint32 FilterSizeY = 5;
|
|
static const uint32 FilterMiddleX = (FilterSizeX - 1) / 2;
|
|
static const uint32 FilterMiddleY = (FilterSizeY - 1) / 2;
|
|
static const uint32 Filter[5][5] =
|
|
{
|
|
{ 58, 85, 96, 85, 58 },
|
|
{ 85, 123, 140, 123, 85 },
|
|
{ 96, 140, 159, 140, 96 },
|
|
{ 85, 123, 140, 123, 85 },
|
|
{ 58, 85, 96, 85, 58 }
|
|
};
|
|
// Gather the filtered samples for this texel.
|
|
for (uint32 FilterY = 0;FilterY < FilterSizeX;FilterY++)
|
|
{
|
|
for (uint32 FilterX = 0;FilterX < FilterSizeY;FilterX++)
|
|
{
|
|
int32 SubX = (int32)X - FilterMiddleX + FilterX,
|
|
SubY = (int32)Y - FilterMiddleY + FilterY;
|
|
if (SubX >= 0 && SubX < (int32)TextureMapping->CachedSizeX && SubY >= 0 && SubY < (int32)TextureMapping->CachedSizeY)
|
|
{
|
|
if (ShadowMapData(SubX,SubY).bIsMapped)
|
|
{
|
|
Visibility += FMath::TruncToInt(Filter[FilterX][FilterY] * ShadowMapData(SubX,SubY).Visibility);
|
|
Coverage += Filter[FilterX][FilterY];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep track of whether any texels have an unoccluded view of the light.
|
|
if (Visibility > 0)
|
|
{
|
|
bIsCompletelyOccluded = false;
|
|
}
|
|
|
|
// Write the filtered shadow-map texel.
|
|
(*FilteredShadowMapData)(X,Y).Visibility = (float)Visibility / (float)Coverage;
|
|
(*FilteredShadowMapData)(X,Y).bIsMapped = true;
|
|
}
|
|
else
|
|
{
|
|
(*FilteredShadowMapData)(X,Y).bIsMapped = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(bIsCompletelyOccluded)
|
|
{
|
|
// If the light is completely occluded, discard the shadow-map.
|
|
delete FilteredShadowMapData;
|
|
FilteredShadowMapData = NULL;
|
|
}
|
|
else
|
|
{
|
|
// Check whether the light should use a light-map or shadow-map.
|
|
const bool bUseStaticLighting = Light->UseStaticLighting();
|
|
if (bUseStaticLighting)
|
|
{
|
|
// Convert the shadow-map into a light-map.
|
|
for (int32 Y = 0;Y < TextureMapping->CachedSizeY;Y++)
|
|
{
|
|
for (int32 X = 0;X < TextureMapping->CachedSizeX;X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
if ((*FilteredShadowMapData)(X,Y).bIsMapped)
|
|
{
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
LightMapData(X,Y).bIsMapped = true;
|
|
|
|
// Compute the light sample for this texel based on the corresponding vertex and its shadow factor.
|
|
float ShadowFactor = (*FilteredShadowMapData)(X,Y).Visibility;
|
|
if (ShadowFactor > 0.0f)
|
|
{
|
|
// Calculate the lighting for the texel.
|
|
check(TexelToVertex.TotalSampleWeight > 0.0f);
|
|
const FStaticLightingVertex CurrentVertex = TexelToVertex.GetVertex();
|
|
const FLinearColor LightIntensity = Light->GetDirectIntensity(CurrentVertex.WorldPosition, false);
|
|
const FGatheredLightSample DirectLighting = CalculatePointLighting(TextureMapping, CurrentVertex, TexelToVertex.ElementIndex, Light, LightIntensity, FLinearColor::White);
|
|
if (GeneralSettings.ViewSingleBounceNumber < 1)
|
|
{
|
|
LightMapData(X,Y).AddWeighted(DirectLighting, ShadowFactor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the light to the light-map's light list.
|
|
LightMapData.AddLight(Light);
|
|
|
|
// Free the shadow-map.
|
|
delete FilteredShadowMapData;
|
|
}
|
|
// only allow for shadow maps if shadow casting is enabled
|
|
else if ((Light->LightFlags & GI_LIGHT_CASTSHADOWS) && (Light->LightFlags & GI_LIGHT_CASTSTATICSHADOWS))
|
|
{
|
|
ShadowMaps.Add(Light,FilteredShadowMapData);
|
|
}
|
|
else
|
|
{
|
|
delete FilteredShadowMapData;
|
|
FilteredShadowMapData = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate lighting from area lights, with filtering in texture space only optionally across severe gradients
|
|
* in the shadow factor. Shadow penumbras will be correctly shaped and will be softer for larger light sources
|
|
* and distant shadow casters.
|
|
*/
|
|
void FStaticLightingSystem::CalculateDirectAreaLightingTextureMapping(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
FShadowMapData2D*& ShadowMapData,
|
|
const FTexelToVertexMap& TexelToVertexMap,
|
|
bool bDebugThisMapping,
|
|
const FLight* Light) const
|
|
{
|
|
LIGHTINGSTAT(FScopedRDTSCTimer AreaShadowsTimer(MappingContext.Stats.AreaShadowsThreadTime));
|
|
|
|
bool bIsCompletelyOccluded = true;
|
|
FLMRandomStream SampleGenerator(0);
|
|
|
|
// Used for the optional lightmap gradient filtering pass
|
|
bool bShadowFactorFilterPassEnabled = false;
|
|
FShadowMapData2D UnfilteredShadowFactorData(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
FShadowMapData2D FilteredShadowFactorData(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
TArray<FLinearColor> TransmissionCache;
|
|
TransmissionCache.Empty(TextureMapping->CachedSizeX * TextureMapping->CachedSizeY);
|
|
TransmissionCache.AddZeroed(TextureMapping->CachedSizeX * TextureMapping->CachedSizeY);
|
|
TArray<FLinearColor> LightIntensityCache;
|
|
LightIntensityCache.Empty(TextureMapping->CachedSizeX * TextureMapping->CachedSizeY);
|
|
LightIntensityCache.AddZeroed(TextureMapping->CachedSizeX * TextureMapping->CachedSizeY);
|
|
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FGatheredLightMapSample& CurrentLightSample = LightMapData(X, Y);
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
|
|
if (CurrentLightSample.bIsMapped
|
|
&& Light->AffectsBounds(FBoxSphereBounds3f(FSphere3f(TexelToVertex.WorldPosition, TexelToVertex.TexelRadius * 2))))
|
|
{
|
|
UnfilteredShadowFactorData(X, Y).bIsMapped = true;
|
|
|
|
if (ShadowMapData)
|
|
{
|
|
FShadowSample& CurrentShadowSample = (*ShadowMapData)(X,Y);
|
|
CurrentShadowSample.bIsMapped = true;
|
|
}
|
|
|
|
// Only continue if some part of the light is in front of the surface
|
|
const FStaticLightingVertex Vertex = TexelToVertex.GetVertex();
|
|
|
|
// @todo: Because we test for rays backfacing the smoothed triangle normal, this code
|
|
// will not skip lighting texels whose tangent space normals are still light-facing,
|
|
// potentially yielding a lighting seam. We should change this code to only cull
|
|
// rays that are backfacing both the tangent space normal and the smoothed vertex normal
|
|
// by a reasonably small threshold, and then make sure the lighting code handles rays
|
|
// that aren't necessarily in front of the triangle robustly.
|
|
//
|
|
// const FVector4f Normal = Vertex.TransformTangentVectorToWorld(TextureMapping->Mesh->EvaluateNormal(Vertex.TextureCoordinates[0], TexelToVertex.ElementIndex)) :*/
|
|
const FVector4f Normal = Vertex.WorldTangentZ;
|
|
|
|
const bool bLightIsInFrontOfTriangle = !Light->BehindSurface(TexelToVertex.WorldPosition, Normal);
|
|
if (bLightIsInFrontOfTriangle || TextureMapping->Mesh->IsTwoSided(TexelToVertex.ElementIndex))
|
|
{
|
|
const FStaticLightingVertex CurrentVertex = TexelToVertex.GetVertex();
|
|
FLinearColor LightIntensity = FLinearColor::White;
|
|
bool bTraceShadowRays = true;
|
|
|
|
// Potentially avoid additional work below if this light has no meaningful contribution
|
|
if (bTraceShadowRays)
|
|
{
|
|
// Compute the incident lighting of the light on the vertex.
|
|
LightIntensity = Light->GetDirectIntensity(CurrentVertex.WorldPosition, false);
|
|
if ((LightIntensity.R <= KINDA_SMALL_NUMBER) &&
|
|
(LightIntensity.G <= KINDA_SMALL_NUMBER) &&
|
|
(LightIntensity.B <= KINDA_SMALL_NUMBER) &&
|
|
(LightIntensity.A <= KINDA_SMALL_NUMBER))
|
|
{
|
|
bTraceShadowRays = false;
|
|
}
|
|
}
|
|
|
|
if (bTraceShadowRays)
|
|
{
|
|
// Approximate the integral over the light's surface to calculate incident direct radiance
|
|
// As AverageVisibility * AverageIncidentRadiance
|
|
//@todo - switch to the physically correct formulation which will allow us to handle area lights correctly,
|
|
// Especially area lights with spatially varying emission
|
|
float ShadowFactor = 0.0f;
|
|
FLinearColor Transmission;
|
|
const TArray<FLightSurfaceSample>& LightSurfaceSamples = Light->GetCachedSurfaceSamples(0, false);
|
|
FLinearColor UnnormalizedTransmission;
|
|
|
|
const FVector2f ShadowValue = CalculatePointAreaShadowing(
|
|
TextureMapping,
|
|
CurrentVertex,
|
|
TexelToVertex.ElementIndex,
|
|
TexelToVertex.TexelRadius,
|
|
Light,
|
|
MappingContext,
|
|
SampleGenerator,
|
|
UnnormalizedTransmission,
|
|
LightSurfaceSamples,
|
|
bDebugThisTexel && GeneralSettings.ViewSingleBounceNumber == 0);
|
|
|
|
if (ShadowValue.X > 0.0f)
|
|
{
|
|
if (ShadowValue.X < ShadowValue.Y)
|
|
{
|
|
// Trace more shadow rays if we are in the penumbra
|
|
const TArray<FLightSurfaceSample>& PenumbraLightSurfaceSamples = Light->GetCachedSurfaceSamples(0, true);
|
|
FLinearColor UnnormalizedPenumbraTransmission;
|
|
|
|
const FVector2f ShadowValuePenumbra = CalculatePointAreaShadowing(
|
|
TextureMapping,
|
|
CurrentVertex,
|
|
TexelToVertex.ElementIndex,
|
|
TexelToVertex.TexelRadius,
|
|
Light,
|
|
MappingContext,
|
|
SampleGenerator,
|
|
UnnormalizedPenumbraTransmission,
|
|
PenumbraLightSurfaceSamples,
|
|
bDebugThisTexel && GeneralSettings.ViewSingleBounceNumber == 0);
|
|
|
|
// Linear combination of uniform and penumbra shadow samples
|
|
ShadowFactor = (ShadowValue.X + ShadowValuePenumbra.X) / (ShadowValue.Y + ShadowValuePenumbra.Y);
|
|
// Weight each transmission by the fraction of total unshadowed rays that contributed to it
|
|
Transmission = (UnnormalizedTransmission + UnnormalizedPenumbraTransmission) / (ShadowValue.Y + ShadowValuePenumbra.Y);
|
|
}
|
|
else
|
|
{
|
|
// The texel is completely out of shadow, fully lit, with an explicit shadow factor of 1.0f
|
|
ShadowFactor = 1.0f;
|
|
Transmission = UnnormalizedTransmission / ShadowValue.Y;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Transmission = FLinearColor::Black;
|
|
// The texel is completely in shadow, with an implicit shadow factor of 0.0f
|
|
}
|
|
|
|
// Cache off the computed values that we'll use later
|
|
checkSlow(TexelToVertex.TotalSampleWeight > 0.0f);
|
|
TransmissionCache[(Y * TextureMapping->CachedSizeX) + X] = Transmission;
|
|
LightIntensityCache[(Y * TextureMapping->CachedSizeX) + X] = LightIntensity;
|
|
// Greyscale transmission for shadowmaps
|
|
UnfilteredShadowFactorData(X, Y).Visibility = ShadowFactor;// * FLinearColorUtils::LinearRGBToXYZ(Transmission).G;
|
|
// We have valid shadow factor values, enable the filter pass
|
|
bShadowFactorFilterPassEnabled = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optional shadow factor filter pass
|
|
if (bShadowFactorFilterPassEnabled && Scene.ShadowSettings.bFilterShadowFactor)
|
|
{
|
|
// Filter in texture space across nearest neighbors
|
|
const float ThresholdForFilteringPenumbra = Scene.ShadowSettings.ShadowFactorGradientTolerance;
|
|
const int32 KernelSizeX = 3; // Expected to be odd
|
|
const int32 KernelSizeY = 3; // Expected to be odd
|
|
const float FilterKernel3x3[KernelSizeX * KernelSizeY] = {
|
|
.5f * 0.150f, .5f * 0.332f, .5f * 0.150f,
|
|
.5f * 0.332f, .5f * 1.000f, .5f * 0.332f,
|
|
.5f * 0.150f, .5f * 0.332f, .5f * 0.150f,
|
|
};
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
// If this texel is valid, look for sharp gradients in nearby texels
|
|
if (UnfilteredShadowFactorData(X, Y).bIsMapped)
|
|
{
|
|
float UnfilteredValue = UnfilteredShadowFactorData(X, Y).Visibility;
|
|
const bool bIntersectingSurface = TexelToVertexMap(X, Y).bIntersectingSurface;
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
const bool bLightIsInFrontOfTriangle = !Light->BehindSurface(TexelToVertex.WorldPosition, TexelToVertex.WorldTangentZ);
|
|
|
|
float FilteredValueNumerator = 0.0f;
|
|
float FilteredValueDenominator = 0.0f;
|
|
float CenterValueWeight = 1.0f;
|
|
|
|
if (ShadowMapData)
|
|
{
|
|
// Lower the self weight on backfaces
|
|
// We want to spread frontface values onto backfaces for shadowmaps where the normal falloff will happen per-pixel
|
|
CenterValueWeight = bLightIsInFrontOfTriangle ? 1.0f : .1f;
|
|
}
|
|
|
|
// Compare (up to) the full grid of adjacent texels
|
|
int32 X1, Y1;
|
|
int32 FilterStepX = ((KernelSizeX - 1) / 2);
|
|
int32 FilterStepY = ((KernelSizeY - 1) / 2);
|
|
|
|
for (int32 KernelIndexY = -FilterStepY; KernelIndexY <= FilterStepY; KernelIndexY++)
|
|
{
|
|
// If this row is out of bounds, skip it
|
|
Y1 = Y + KernelIndexY;
|
|
if ((Y1 < 0) ||
|
|
(Y1 > (TextureMapping->CachedSizeY - 1)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (int32 KernelIndexX = -FilterStepX; KernelIndexX <= FilterStepX; KernelIndexX++)
|
|
{
|
|
// If this row is out of bounds, skip it
|
|
X1 = X + KernelIndexX;
|
|
if ((X1 < 0) ||
|
|
(X1 > (TextureMapping->CachedSizeX - 1)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only include the texel if it's not completely in shadow
|
|
if (UnfilteredShadowFactorData(X1, Y1).bIsMapped
|
|
&& !(X1 == X && Y1 == Y)
|
|
// Don't filter across intersecting surface boundaries
|
|
&& (bIntersectingSurface == TexelToVertexMap(X1, Y1).bIntersectingSurface))
|
|
{
|
|
float ComparisonValue = UnfilteredShadowFactorData(X1, Y1).Visibility;
|
|
float DifferenceValue = FMath::Abs(UnfilteredValue - ComparisonValue);
|
|
const FTexelToVertex& NeighborTexelToVertex = TexelToVertexMap(X1, Y1);
|
|
const bool bNeighborLightIsInFrontOfTriangle = !Light->BehindSurface(NeighborTexelToVertex.WorldPosition, NeighborTexelToVertex.WorldTangentZ);
|
|
|
|
if (DifferenceValue > ThresholdForFilteringPenumbra
|
|
// If we are filtering shadow factors for a shadowmap, only gather shadow values from frontfaces
|
|
&& (!ShadowMapData || bNeighborLightIsInFrontOfTriangle))
|
|
{
|
|
int32 FilterKernelIndex = ((KernelIndexY + FilterStepY) * KernelSizeX) + (KernelIndexX + FilterStepX);
|
|
float FilterKernelValue = FilterKernel3x3[FilterKernelIndex];
|
|
|
|
FilteredValueNumerator += (ComparisonValue * FilterKernelValue);
|
|
FilteredValueDenominator += FilterKernelValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
float FinalShadowFactorValue;
|
|
if (FilteredValueDenominator > 0.0f)
|
|
{
|
|
FinalShadowFactorValue = (FilteredValueNumerator + UnfilteredValue * CenterValueWeight) / (FilteredValueDenominator + CenterValueWeight);
|
|
}
|
|
else
|
|
{
|
|
FinalShadowFactorValue = UnfilteredValue;
|
|
}
|
|
|
|
FilteredShadowFactorData(X, Y).Visibility = FinalShadowFactorValue;
|
|
FilteredShadowFactorData(X, Y).bIsMapped = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 NumUnoccludedTexels = 0;
|
|
int32 NumMappedTexels = 0;
|
|
if (bShadowFactorFilterPassEnabled)
|
|
{
|
|
LIGHTINGSTAT(FScopedRDTSCTimer AreaLightingTimer(MappingContext.Stats.AreaLightingThreadTime));
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
float ShadowFactor;
|
|
bool bIsMapped;
|
|
if (Scene.ShadowSettings.bFilterShadowFactor)
|
|
{
|
|
bIsMapped = FilteredShadowFactorData(X, Y).bIsMapped;
|
|
ShadowFactor = FilteredShadowFactorData(X, Y).Visibility;
|
|
}
|
|
else
|
|
{
|
|
bIsMapped = UnfilteredShadowFactorData(X, Y).bIsMapped;
|
|
ShadowFactor = UnfilteredShadowFactorData(X, Y).Visibility;
|
|
}
|
|
|
|
NumMappedTexels += bIsMapped ? 1 : 0;
|
|
if (bIsMapped && ShadowFactor > 0.0f)
|
|
{
|
|
NumUnoccludedTexels++;
|
|
// Get any cached values
|
|
const float AdjustedShadowFactor = FMath::Pow(ShadowFactor, Light->ShadowExponent);
|
|
|
|
if (GeneralSettings.ViewSingleBounceNumber < 1)
|
|
{
|
|
if (ShadowMapData)
|
|
{
|
|
FShadowSample& CurrentShadowSample = (*ShadowMapData)(X,Y);
|
|
CurrentShadowSample.Visibility = AdjustedShadowFactor;
|
|
if ( CurrentShadowSample.Visibility > 0.0001f )
|
|
{
|
|
bIsCompletelyOccluded = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Calculate any derived values
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
const FStaticLightingVertex CurrentVertex = TexelToVertex.GetVertex();
|
|
const FLinearColor LightIntensity = LightIntensityCache[(Y * TextureMapping->CachedSizeX) + X];
|
|
const FLinearColor Transmission = TransmissionCache[(Y * TextureMapping->CachedSizeX) + X];
|
|
const FGatheredLightSample DirectLighting = CalculatePointLighting(TextureMapping, CurrentVertex, TexelToVertex.ElementIndex, Light, LightIntensity, Transmission);
|
|
|
|
FGatheredLightMapSample& CurrentLightSample = LightMapData(X,Y);
|
|
CurrentLightSample.AddWeighted(DirectLighting, AdjustedShadowFactor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ShadowMapData && (bIsCompletelyOccluded || NumUnoccludedTexels < NumMappedTexels * ShadowSettings.MinUnoccludedFraction))
|
|
{
|
|
delete ShadowMapData;
|
|
ShadowMapData = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sample data for the low and high resolution source data that the distance field for shadowing is generated off of.
|
|
* The defaults for all members are implicitly 0 since any uses of this class zero the memory after allocating it.
|
|
*/
|
|
class FVisibilitySample
|
|
{
|
|
protected:
|
|
/** World space position in XYZ, Distance to the nearest occluder in W, only valid if !bVisible. */
|
|
FVector4f PositionAndOccluderDistance;
|
|
/** World space normal */
|
|
float NormalX, NormalY, NormalZ;
|
|
/** Whether this sample is visible to the light. */
|
|
uint32 bVisible : 1;
|
|
/** True if this sample maps to a valid point on a surface. */
|
|
uint32 bIsMapped : 1;
|
|
/** Whether this sample needs high resolution sampling. */
|
|
uint32 bNeedsHighResSampling : 1;
|
|
|
|
public:
|
|
inline FVector4f GetPosition() const { return FVector4f(PositionAndOccluderDistance, 0.0f); }
|
|
inline float GetOccluderDistance() const { return PositionAndOccluderDistance.W; }
|
|
inline FVector4f GetNormal() const { return FVector4f(NormalX, NormalY, NormalZ); }
|
|
inline bool IsVisible() const { return bVisible; }
|
|
inline bool IsMapped() const { return bIsMapped; }
|
|
inline bool NeedsHighResSampling() const { return bNeedsHighResSampling; }
|
|
|
|
inline void SetPosition(const FVector4f& InPosition)
|
|
{
|
|
PositionAndOccluderDistance.X = InPosition.X;
|
|
PositionAndOccluderDistance.Y = InPosition.Y;
|
|
PositionAndOccluderDistance.Z = InPosition.Z;
|
|
}
|
|
inline void SetOccluderDistance(float InOccluderDistance)
|
|
{
|
|
PositionAndOccluderDistance.W = InOccluderDistance;
|
|
}
|
|
inline void SetNormal(const FVector4f& InNormal)
|
|
{
|
|
NormalX = InNormal.X;
|
|
NormalY = InNormal.Y;
|
|
NormalZ = InNormal.Z;
|
|
}
|
|
inline void SetVisible(bool bInVisible) { bVisible = bInVisible; }
|
|
inline void SetMapped(bool bInMapped) { bIsMapped = bInMapped; }
|
|
};
|
|
|
|
/**
|
|
* Sample data for the low resolution visibility data that is populated initially for distance field generation.
|
|
* Each low resolution sample contains a set of high resolution samples if the low resolution sample is next to a shadow transition.
|
|
*/
|
|
class FLowResolutionVisibilitySample : public FVisibilitySample
|
|
{
|
|
public:
|
|
uint16 ElementIndex;
|
|
|
|
/** High resolution samples corresponding to this low resolution sample, only allocated if bNeedsHighResSampling == true. */
|
|
TArray<FVisibilitySample> HighResolutionSamples;
|
|
|
|
inline void SetNeedsHighResSampling(bool bInNeedsHighResSampling, int32 UpsampleFactor)
|
|
{
|
|
if (bInNeedsHighResSampling)
|
|
{
|
|
HighResolutionSamples.Empty(UpsampleFactor * UpsampleFactor);
|
|
HighResolutionSamples.AddZeroed(UpsampleFactor * UpsampleFactor);
|
|
}
|
|
bNeedsHighResSampling = bInNeedsHighResSampling;
|
|
}
|
|
};
|
|
|
|
/** 2D array of FLowResolutionVisibilitySample's */
|
|
class FTexelVisibilityData2D : public FShadowMapData2DData
|
|
{
|
|
public:
|
|
FTexelVisibilityData2D(uint32 InSizeX,uint32 InSizeY) :
|
|
FShadowMapData2DData(InSizeX, InSizeY)
|
|
{
|
|
Data.Empty(InSizeX * InSizeY);
|
|
Data.AddZeroed(InSizeX * InSizeY);
|
|
}
|
|
|
|
// Accessors.
|
|
const FLowResolutionVisibilitySample& operator()(uint32 X,uint32 Y) const { return Data[SizeX * Y + X]; }
|
|
FLowResolutionVisibilitySample& operator()(uint32 X,uint32 Y) { return Data[SizeX * Y + X]; }
|
|
uint32 GetSizeX() const { return SizeX; }
|
|
uint32 GetSizeY() const { return SizeY; }
|
|
void Empty() { Data.Empty(); }
|
|
SIZE_T GetAllocatedSize() const { return Data.GetAllocatedSize(); }
|
|
|
|
private:
|
|
TArray<FLowResolutionVisibilitySample> Data;
|
|
};
|
|
|
|
class FDistanceFieldRasterPolicy
|
|
{
|
|
public:
|
|
|
|
typedef FStaticLightingInterpolant InterpolantType;
|
|
|
|
/** Initialization constructor. */
|
|
FDistanceFieldRasterPolicy(FTexelVisibilityData2D& InLowResolutionVisibilityData, int32 InUpsampleFactor, int32 InSizeX, int32 InSizeY) :
|
|
LowResolutionVisibilityData(InLowResolutionVisibilityData),
|
|
UpsampleFactor(InUpsampleFactor),
|
|
SizeX(InSizeX),
|
|
SizeY(InSizeY)
|
|
{}
|
|
|
|
protected:
|
|
|
|
// FTriangleRasterizer policy interface.
|
|
|
|
int32 GetMinX() const { return 0; }
|
|
int32 GetMaxX() const { return SizeX - 1; }
|
|
int32 GetMinY() const { return 0; }
|
|
int32 GetMaxY() const { return SizeY - 1; }
|
|
|
|
void ProcessPixel(int32 X,int32 Y,const InterpolantType& Interpolant,bool BackFacing);
|
|
|
|
private:
|
|
|
|
FTexelVisibilityData2D& LowResolutionVisibilityData;
|
|
const int32 UpsampleFactor;
|
|
const int32 SizeX;
|
|
const int32 SizeY;
|
|
};
|
|
|
|
void FDistanceFieldRasterPolicy::ProcessPixel(int32 X,int32 Y,const InterpolantType& Interpolant,bool BackFacing)
|
|
{
|
|
FLowResolutionVisibilitySample& LowResSample = LowResolutionVisibilityData(X / UpsampleFactor, Y / UpsampleFactor);
|
|
LowResSample.ElementIndex = Interpolant.ElementIndex;
|
|
if (LowResSample.NeedsHighResSampling())
|
|
{
|
|
FVisibilitySample& Sample = LowResSample.HighResolutionSamples[Y % UpsampleFactor * UpsampleFactor + X % UpsampleFactor];
|
|
Sample.SetPosition(Interpolant.Vertex.WorldPosition);
|
|
Sample.SetNormal(Interpolant.Vertex.WorldTangentZ);
|
|
Sample.SetMapped(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate signed distance field shadowing from a single light,
|
|
* Based on the paper "Improved Alpha-Tested Magnification for Vector Textures and Special Effects" by Valve.
|
|
*/
|
|
void FStaticLightingSystem::CalculateDirectSignedDistanceFieldLightingTextureMappingTextureSpace(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
TMap<const FLight*, FSignedDistanceFieldShadowMapData2D*>& ShadowMaps,
|
|
const FTexelToVertexMap& TexelToVertexMap,
|
|
const FTexelToCornersMap& TexelToCornersMap,
|
|
bool bDebugThisMapping,
|
|
const FLight* Light) const
|
|
{
|
|
LIGHTINGSTAT(FManualRDTSCTimer FirstPassSourceTimer(MappingContext.Stats.SignedDistanceFieldSourceFirstPassThreadTime));
|
|
TArray<FStaticLightingInterpolant> MeshVertices;
|
|
MeshVertices.Empty(TextureMapping->Mesh->NumTriangles * 3);
|
|
MeshVertices.AddZeroed(TextureMapping->Mesh->NumTriangles * 3);
|
|
float AverageTexelDensity = 0.0f;
|
|
for (int32 TriangleIndex = 0; TriangleIndex < TextureMapping->Mesh->NumTriangles; TriangleIndex++)
|
|
{
|
|
// Query the mesh for the triangle's vertices.
|
|
int32 Element;
|
|
TextureMapping->Mesh->GetTriangle(TriangleIndex, MeshVertices[TriangleIndex * 3].Vertex, MeshVertices[TriangleIndex * 3 + 1].Vertex, MeshVertices[TriangleIndex * 3 + 2].Vertex, Element);
|
|
MeshVertices[TriangleIndex * 3].ElementIndex = MeshVertices[TriangleIndex * 3 + 1].ElementIndex = MeshVertices[TriangleIndex * 3 + 2].ElementIndex = Element;
|
|
|
|
const FVector4f TriangleNormal = (MeshVertices[TriangleIndex * 3 + 2].Vertex.WorldPosition - MeshVertices[TriangleIndex * 3].Vertex.WorldPosition) ^ (MeshVertices[TriangleIndex * 3 + 1].Vertex.WorldPosition - MeshVertices[TriangleIndex].Vertex.WorldPosition);
|
|
const float TriangleArea = 0.5f * TriangleNormal.Size3();
|
|
|
|
if (TriangleArea > DELTA)
|
|
{
|
|
// Triangle vertices in lightmap UV space, scaled by the lightmap resolution
|
|
const FVector2f Vertex0 = MeshVertices[TriangleIndex * 3 + 0].Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
const FVector2f Vertex1 = MeshVertices[TriangleIndex * 3 + 1].Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
const FVector2f Vertex2 = MeshVertices[TriangleIndex * 3 + 2].Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
|
|
// Area in lightmap space, or the number of lightmap texels covered by this triangle
|
|
const float LightmapTriangleArea = FMath::Abs(
|
|
Vertex0.X * (Vertex1.Y - Vertex2.Y)
|
|
+ Vertex1.X * (Vertex2.Y - Vertex0.Y)
|
|
+ Vertex2.X * (Vertex0.Y - Vertex1.Y));
|
|
|
|
// Accumulate the texel density
|
|
AverageTexelDensity += LightmapTriangleArea / TriangleArea;
|
|
}
|
|
}
|
|
|
|
int32 UpsampleFactor = 1;
|
|
if (AverageTexelDensity > DELTA)
|
|
{
|
|
// Normalize the average
|
|
AverageTexelDensity /= TextureMapping->Mesh->NumTriangles;
|
|
// Calculate the length of one side of a right isosceles triangle with texel density equal to the mesh's average texel density
|
|
const float RightTriangleSide = FMath::Sqrt(2.0f * AverageTexelDensity);
|
|
// Choose an upsample factor based on the average texels/world space ratio
|
|
// The result is that small, high resolution meshes will not upsample as much, since they don't need it,
|
|
// But large, low resolution meshes will upsample a lot.
|
|
const int32 TargetUpsampleFactor = FMath::TruncToInt(ShadowSettings.ApproximateHighResTexelsPerMaxTransitionDistance / (RightTriangleSide * ShadowSettings.MaxTransitionDistanceWorldSpace));
|
|
// Round up to the nearest odd factor, so each destination texel has a high resolution source texel at its center
|
|
// Clamp the upscale factor to be less than 13, since the quality improvements of upsampling higher than that are negligible.
|
|
UpsampleFactor = FMath::Clamp(TargetUpsampleFactor - TargetUpsampleFactor % 2 + 1, ShadowSettings.MinDistanceFieldUpsampleFactor, 13);
|
|
}
|
|
MappingContext.Stats.AccumulatedSignedDistanceFieldUpsampleFactors += UpsampleFactor;
|
|
MappingContext.Stats.NumSignedDistanceFieldCalculations++;
|
|
|
|
bool bIsCompletelyOccluded = true;
|
|
int32 NumUnoccludedTexels = 0;
|
|
int32 NumMappedTexels = 0;
|
|
// Calculate visibility at the resolution of the final distance field in a first pass
|
|
FTexelVisibilityData2D LowResolutionVisibilityData(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
if (TexelToVertex.TotalSampleWeight > 0.0f)
|
|
{
|
|
NumMappedTexels++;
|
|
// Note: not checking for backfacing normals because some of the high resolution samples corresponding to this texel may be frontfacing
|
|
if (Light->AffectsBounds(FBoxSphereBounds3f(TexelToVertex.WorldPosition, FVector4f(0,0,0),0)))
|
|
{
|
|
FLowResolutionVisibilitySample& CurrentSample = LowResolutionVisibilityData(X, Y);
|
|
CurrentSample.SetPosition(TexelToVertex.WorldPosition);
|
|
CurrentSample.SetNormal(TexelToVertex.WorldTangentZ);
|
|
// Only mark the texel as mapped if we are inside the light's influence
|
|
// This is important because stationary lights are assigned shadowmap channels based on overlap,
|
|
// And multiple shadowmaps on the same object may be merged together, but only if each one marks the area that it has valid data
|
|
CurrentSample.SetMapped(true);
|
|
|
|
const FVector4f LightPosition = Light->LightCenterPosition(TexelToVertex.WorldPosition, TexelToVertex.WorldTangentZ);
|
|
const FVector4f LightVector = (LightPosition - TexelToVertex.WorldPosition).GetSafeNormal();
|
|
|
|
FVector4f NormalForOffset = CurrentSample.GetNormal();
|
|
// 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.
|
|
const bool bIsTwoSided = TextureMapping->Mesh->IsTwoSided(CurrentSample.ElementIndex);
|
|
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.
|
|
TexelToVertex.WorldPosition
|
|
+ LightVector * SceneConstants.VisibilityRayOffsetDistance
|
|
+ NormalForOffset * SceneConstants.VisibilityNormalOffsetDistance,
|
|
LightPosition,
|
|
TextureMapping,
|
|
Light
|
|
);
|
|
|
|
FLightRayIntersection Intersection;
|
|
MappingContext.Stats.NumSignedDistanceFieldAdaptiveSourceRaysFirstPass++;
|
|
// Could trace a boolean visibility ray, no other information is needed,
|
|
// However FStaticLightingAggregateMesh::IntersectLightRay currently does not handle masked materials correctly with boolean visibility rays.
|
|
AggregateMesh->IntersectLightRay(LightRay, true, false, true, MappingContext.RayCache, Intersection);
|
|
if (!Intersection.bIntersects)
|
|
{
|
|
NumUnoccludedTexels++;
|
|
bIsCompletelyOccluded = false;
|
|
CurrentSample.SetVisible(true);
|
|
}
|
|
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisTexel && GeneralSettings.ViewSingleBounceNumber == 0)
|
|
{
|
|
FDebugStaticLightingRay DebugRay(LightRay.Start, LightRay.End, Intersection.bIntersects);
|
|
if (Intersection.bIntersects)
|
|
{
|
|
DebugRay.End = Intersection.IntersectionVertex.WorldPosition;
|
|
}
|
|
MappingContext.DebugOutput->ShadowRays.Add(DebugRay);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
FirstPassSourceTimer.Stop();
|
|
|
|
if (!bIsCompletelyOccluded && NumUnoccludedTexels > NumMappedTexels * ShadowSettings.MinUnoccludedFraction)
|
|
{
|
|
LIGHTINGSTAT(FManualRDTSCTimer SecondPassSourceTimer(MappingContext.Stats.SignedDistanceFieldSourceSecondPassThreadTime));
|
|
check(UpsampleFactor % 2 == 1 && UpsampleFactor >= 1);
|
|
const int32 HighResolutionSignalSizeX = TextureMapping->CachedSizeX * UpsampleFactor;
|
|
const int32 HighResolutionSignalSizeY = TextureMapping->CachedSizeY * UpsampleFactor;
|
|
// Allocate the final distance field shadow map on the heap, since it will be passed out of this function
|
|
FSignedDistanceFieldShadowMapData2D* ShadowMapData = new FSignedDistanceFieldShadowMapData2D(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
|
|
// Neighbor texel coordinates - the order in which these are stored matters later
|
|
const FIntPoint Neighbors[] =
|
|
{
|
|
FIntPoint(0, 1),
|
|
FIntPoint(0, -1),
|
|
FIntPoint(1, 0),
|
|
FIntPoint(-1, 0)
|
|
};
|
|
|
|
// Offsets to the high resolution samples corresponding to the corners of a low resolution sample
|
|
const FIntPoint Corners[] =
|
|
{
|
|
FIntPoint(0, 0),
|
|
FIntPoint(0, UpsampleFactor - 1),
|
|
FIntPoint(UpsampleFactor - 1, 0),
|
|
FIntPoint(UpsampleFactor - 1, UpsampleFactor - 1)
|
|
};
|
|
|
|
// Traverse the visibility data collected at the resolution of the final distance field, detecting where additional sampling is required.
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FLowResolutionVisibilitySample& CurrentSample = LowResolutionVisibilityData(X, Y);
|
|
if (CurrentSample.IsMapped())
|
|
{
|
|
FSignedDistanceFieldShadowSample& FinalShadowSample = (*ShadowMapData)(X, Y);
|
|
FinalShadowSample.bIsMapped = true;
|
|
if (CurrentSample.IsVisible())
|
|
{
|
|
// Initialize the final distance field data, since it will only be written to after this if it gets scattered to during the search.
|
|
FinalShadowSample.Distance = 1.0f;
|
|
FinalShadowSample.PenumbraSize = 1.0f;
|
|
}
|
|
|
|
// Search for a neighbor with different visibility
|
|
bool bNeighborsDifferent = false;
|
|
for (int32 i = 0 ; i < UE_ARRAY_COUNT(Neighbors); i++)
|
|
{
|
|
if (X + Neighbors[i].X > 0
|
|
&& X + Neighbors[i].X < TextureMapping->CachedSizeX
|
|
&& Y + Neighbors[i].Y > 0
|
|
&& Y + Neighbors[i].Y < TextureMapping->CachedSizeY)
|
|
{
|
|
const FLowResolutionVisibilitySample& NeighborSample = LowResolutionVisibilityData(X + Neighbors[i].X, Y + Neighbors[i].Y);
|
|
if (CurrentSample.IsVisible() != NeighborSample.IsVisible() && NeighborSample.IsMapped())
|
|
{
|
|
bNeighborsDifferent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark the low resolution sample as needing high resolution sampling, since it is next to a shadow transition
|
|
if (bNeighborsDifferent)
|
|
{
|
|
CurrentSample.SetNeedsHighResSampling(bNeighborsDifferent, UpsampleFactor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FDistanceFieldRasterPolicy RasterPolicy(LowResolutionVisibilityData, UpsampleFactor, HighResolutionSignalSizeX, HighResolutionSignalSizeY);
|
|
FTriangleRasterizer<FDistanceFieldRasterPolicy> DistanceFieldRasterizer(RasterPolicy);
|
|
// Rasterize the mesh at the upsampled source data resolution
|
|
for (int32 TriangleIndex = 0; TriangleIndex < MeshVertices.Num() / 3; TriangleIndex++)
|
|
{
|
|
const FStaticLightingInterpolant& V0 = MeshVertices[TriangleIndex * 3];
|
|
const FStaticLightingInterpolant& V1 = MeshVertices[TriangleIndex * 3 + 1];
|
|
const FStaticLightingInterpolant& V2 = MeshVertices[TriangleIndex * 3 + 2];
|
|
|
|
DistanceFieldRasterizer.DrawTriangle(
|
|
V0,
|
|
V1,
|
|
V2,
|
|
V0.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(HighResolutionSignalSizeX, HighResolutionSignalSizeY) + FVector2f(-0.5f,-0.5f),
|
|
V1.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(HighResolutionSignalSizeX, HighResolutionSignalSizeY) + FVector2f(-0.5f,-0.5f),
|
|
V2.Vertex.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex] * FVector2f(HighResolutionSignalSizeX, HighResolutionSignalSizeY) + FVector2f(-0.5f,-0.5f),
|
|
false
|
|
);
|
|
}
|
|
MeshVertices.Empty();
|
|
|
|
// Check for edge cases where the low resolution sample is mapped, but none of the high resolution samples got mapped.
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FLowResolutionVisibilitySample& CurrentSample = LowResolutionVisibilityData(X, Y);
|
|
if (CurrentSample.IsMapped() && CurrentSample.NeedsHighResSampling())
|
|
{
|
|
bool bAnyHighResSamplesMapped = false;
|
|
// Iterate over all the upsampled source data texels corresponding to this texel
|
|
for (int32 HighResY = 0; HighResY < UpsampleFactor; HighResY++)
|
|
{
|
|
for (int32 HighResX = 0; HighResX < UpsampleFactor; HighResX++)
|
|
{
|
|
FVisibilitySample& CurrentHighResSample = CurrentSample.HighResolutionSamples[HighResY * UpsampleFactor + HighResX];
|
|
if (CurrentHighResSample.IsMapped())
|
|
{
|
|
bAnyHighResSamplesMapped = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If none of the high res samples are mapped, but the low resolution sample is mapped,
|
|
// Propagate the low resolution corner information to the corresponding high resolution samples.
|
|
// This handles texels along UV seams where only the corner of the texel is mapped.
|
|
if (!bAnyHighResSamplesMapped)
|
|
{
|
|
const FTexelToCorners& TexelToCorners = TexelToCornersMap(X, Y);
|
|
for (int32 CornerIndex = 0; CornerIndex < UE_ARRAY_COUNT(Corners); CornerIndex++)
|
|
{
|
|
if (TexelToCorners.bValid[CornerIndex])
|
|
{
|
|
FVisibilitySample& CornerHighResSample = CurrentSample.HighResolutionSamples[Corners[CornerIndex].Y * UpsampleFactor + Corners[CornerIndex].X];
|
|
CornerHighResSample.SetMapped(true);
|
|
CornerHighResSample.SetPosition(TexelToCorners.Corners[CornerIndex].WorldPosition);
|
|
CornerHighResSample.SetNormal(TexelToCorners.WorldTangentZ);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FLowResolutionVisibilitySample& CurrentSample = LowResolutionVisibilityData(X, Y);
|
|
// Do high resolution sampling if necessary
|
|
if (CurrentSample.IsMapped() && CurrentSample.NeedsHighResSampling())
|
|
{
|
|
const bool bIsTwoSided = TextureMapping->Mesh->IsTwoSided(CurrentSample.ElementIndex);
|
|
for (int32 HighResY = 0; HighResY < UpsampleFactor; HighResY++)
|
|
{
|
|
for (int32 HighResX = 0; HighResX < UpsampleFactor; HighResX++)
|
|
{
|
|
FVisibilitySample& HighResSample = CurrentSample.HighResolutionSamples[HighResY * UpsampleFactor + HighResX];
|
|
const bool bLightIsInFrontOfTriangle = !IsLightBehindSurface(HighResSample.GetPosition(),HighResSample.GetNormal(),Light);
|
|
|
|
if ((bLightIsInFrontOfTriangle || bIsTwoSided)
|
|
&& Light->AffectsBounds(FBoxSphereBounds3f(HighResSample.GetPosition(), FVector4f(0,0,0),0)))
|
|
{
|
|
const FVector4f LightPosition = Light->LightCenterPosition(HighResSample.GetPosition(), HighResSample.GetNormal());
|
|
const FVector4f LightVector = (LightPosition - HighResSample.GetPosition()).GetSafeNormal();
|
|
|
|
FVector4f NormalForOffset = HighResSample.GetNormal();
|
|
// 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.
|
|
HighResSample.GetPosition()
|
|
+ LightVector * SceneConstants.VisibilityRayOffsetDistance
|
|
+ NormalForOffset * SceneConstants.VisibilityNormalOffsetDistance,
|
|
LightPosition,
|
|
TextureMapping,
|
|
Light
|
|
);
|
|
|
|
FLightRayIntersection Intersection;
|
|
MappingContext.Stats.NumSignedDistanceFieldAdaptiveSourceRaysSecondPass++;
|
|
// Have to calculate the closest intersection so we know the distance to the nearest occluder
|
|
//@todo - for the occluder distance to be correct, the ray should actually go from the light to the receiver
|
|
AggregateMesh->IntersectLightRay(LightRay, true, false, true, MappingContext.RayCache, Intersection);
|
|
if (Intersection.bIntersects)
|
|
{
|
|
HighResSample.SetOccluderDistance((LightRay.Start - Intersection.IntersectionVertex.WorldPosition).Size3());
|
|
}
|
|
else
|
|
{
|
|
HighResSample.SetVisible(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
SecondPassSourceTimer.Stop();
|
|
|
|
int32 NumScattersToSelectedTexel = 0;
|
|
LIGHTINGSTAT(FScopedRDTSCTimer SearchTimer(MappingContext.Stats.SignedDistanceFieldSearchThreadTime));
|
|
// Traverse the high resolution source data by going over low res samples that that need high resolution sampling, and at each texel that is next to a transition,
|
|
// Scatter the distance to that texel onto all low resolution distance field texels within a certain world space distance from the transition texel.
|
|
// The end result is that each low resolution texel in the distance field has the world space distance to the nearest transition in the high resolution visibility data.
|
|
// Using a scatter from the high res transition texels is significantly faster than a brute force gather from the low resolution distance field texels,
|
|
// Because only a small set of the high resolution texels are next to the shadow transition.
|
|
for (int32 LowResY = 0; LowResY < TextureMapping->CachedSizeY; LowResY++)
|
|
{
|
|
for (int32 LowResX = 0; LowResX < TextureMapping->CachedSizeX; LowResX++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& LowResY == Scene.DebugInput.LocalY
|
|
&& LowResX == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FLowResolutionVisibilitySample& CurrentLowResSample = LowResolutionVisibilityData(LowResX, LowResY);
|
|
if (CurrentLowResSample.IsMapped() && CurrentLowResSample.NeedsHighResSampling())
|
|
{
|
|
for (int32 HighResY = 0; HighResY < UpsampleFactor; HighResY++)
|
|
{
|
|
for (int32 HighResX = 0; HighResX < UpsampleFactor; HighResX++)
|
|
{
|
|
FVisibilitySample& HighResSample = CurrentLowResSample.HighResolutionSamples[HighResY * UpsampleFactor + HighResX];
|
|
// Only texels that needed high resolution sampling can be next to the shadow transition
|
|
// Only operate on shadowed texels, since they know the distance to the nearest occluder, which is necessary for calculating penumbra size
|
|
// As a result, the reconstructed shadow transition will be slightly offset
|
|
if (HighResSample.IsMapped() && !HighResSample.IsVisible())
|
|
{
|
|
// Detect texels next to the shadow transition
|
|
bool bNeighborsDifferent = false;
|
|
for (int32 i = 0 ; i < UE_ARRAY_COUNT(Neighbors); i++)
|
|
{
|
|
// Calculate the high resolution indices, which may go into neighboring low resolution samples
|
|
const int32 HighResNeighborX = LowResX * UpsampleFactor + HighResX + Neighbors[i].X;
|
|
const int32 HighResNeighborY = LowResY * UpsampleFactor + HighResY + Neighbors[i].Y;
|
|
const int32 LowResNeighborX = HighResNeighborX / UpsampleFactor;
|
|
const int32 LowResNeighborY = HighResNeighborY / UpsampleFactor;
|
|
if (LowResNeighborX > 0
|
|
&& LowResNeighborX < TextureMapping->CachedSizeX
|
|
&& LowResNeighborY > 0
|
|
&& LowResNeighborY < TextureMapping->CachedSizeY)
|
|
{
|
|
const FLowResolutionVisibilitySample& LowResNeighborSample = LowResolutionVisibilityData(LowResNeighborX, LowResNeighborY);
|
|
// If the low res neighbor sample has high resolution samples, check the neighboring high resolution sample's visibility
|
|
if (LowResNeighborSample.NeedsHighResSampling())
|
|
{
|
|
const FVisibilitySample& HighResNeighborSample = LowResNeighborSample.HighResolutionSamples[(HighResNeighborY % UpsampleFactor) * UpsampleFactor + HighResNeighborX % UpsampleFactor];
|
|
if (HighResNeighborSample.IsMapped() && HighResNeighborSample.IsVisible())
|
|
{
|
|
bNeighborsDifferent = true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The low res neighbor sample didn't have high resolution samples, use its visibility
|
|
if (LowResNeighborSample.IsMapped() && LowResNeighborSample.IsVisible())
|
|
{
|
|
bNeighborsDifferent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bNeighborsDifferent)
|
|
{
|
|
float WorldSpacePerHighResTexelX = FLT_MAX;
|
|
float WorldSpacePerHighResTexelY = FLT_MAX;
|
|
// Determine how far to scatter transition distance by measuring the world space distance between this texel and its neighbors
|
|
for (int32 i = 0 ; i < UE_ARRAY_COUNT(Neighbors); i++)
|
|
{
|
|
if (HighResX + Neighbors[i].X > 0
|
|
&& HighResX + Neighbors[i].X < UpsampleFactor
|
|
&& HighResY + Neighbors[i].Y > 0
|
|
&& HighResY + Neighbors[i].Y < UpsampleFactor)
|
|
{
|
|
const FVisibilitySample& NeighborSample = CurrentLowResSample.HighResolutionSamples[(HighResY + Neighbors[i].Y) * UpsampleFactor + HighResX + Neighbors[i].X];
|
|
if (NeighborSample.IsMapped())
|
|
{
|
|
// Last two neighbor offsets are in X
|
|
if (i >= 2)
|
|
{
|
|
WorldSpacePerHighResTexelX = FMath::Min(WorldSpacePerHighResTexelX, (NeighborSample.GetPosition() - HighResSample.GetPosition()).Size3());
|
|
}
|
|
else
|
|
{
|
|
WorldSpacePerHighResTexelY = FMath::Min(WorldSpacePerHighResTexelY, (NeighborSample.GetPosition() - HighResSample.GetPosition()).Size3());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (WorldSpacePerHighResTexelX == FLT_MAX && WorldSpacePerHighResTexelY == FLT_MAX)
|
|
{
|
|
WorldSpacePerHighResTexelX = 1.0f;
|
|
WorldSpacePerHighResTexelY = 1.0f;
|
|
}
|
|
else if (WorldSpacePerHighResTexelX == FLT_MAX)
|
|
{
|
|
WorldSpacePerHighResTexelX = WorldSpacePerHighResTexelY;
|
|
}
|
|
else if (WorldSpacePerHighResTexelY == FLT_MAX)
|
|
{
|
|
WorldSpacePerHighResTexelY = WorldSpacePerHighResTexelX;
|
|
}
|
|
|
|
// Scatter to all distance field texels within MaxTransitionDistanceWorldSpace, rounded up.
|
|
// This is an approximation to the actual set of distance field texels that are within MaxTransitionDistanceWorldSpace that tends to work out well.
|
|
// Apply a clamp to avoid a performance cliff with some texels, whose adjacent texel in lightmap space is actually far away in world space
|
|
const int32 NumLowResScatterTexelsY = FMath::Min(FMath::TruncToInt(ShadowSettings.MaxTransitionDistanceWorldSpace / (WorldSpacePerHighResTexelY * UpsampleFactor)) + 1, 100);
|
|
const int32 NumLowResScatterTexelsX = FMath::Min(FMath::TruncToInt(ShadowSettings.MaxTransitionDistanceWorldSpace / (WorldSpacePerHighResTexelX * UpsampleFactor)) + 1, 100);
|
|
MappingContext.Stats.NumSignedDistanceFieldScatters++;
|
|
for (int32 ScatterOffsetY = -NumLowResScatterTexelsY; ScatterOffsetY <= NumLowResScatterTexelsY; ScatterOffsetY++)
|
|
{
|
|
const int32 LowResScatterY = LowResY + ScatterOffsetY;
|
|
if (LowResScatterY < 0 || LowResScatterY >= TextureMapping->CachedSizeY)
|
|
{
|
|
continue;
|
|
}
|
|
for (int32 ScatterOffsetX = -NumLowResScatterTexelsX; ScatterOffsetX <= NumLowResScatterTexelsX; ScatterOffsetX++)
|
|
{
|
|
const int32 LowResScatterX = LowResX + ScatterOffsetX;
|
|
if (LowResScatterX < 0 || LowResScatterX >= TextureMapping->CachedSizeX)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool bDebugThisScatterTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
// Debug when the selected texel is being scattered to
|
|
// This may get hit any number of times, only the closest transition distance will be kept in the end
|
|
if (bDebugThisMapping
|
|
&& LowResScatterY == Scene.DebugInput.LocalY
|
|
&& LowResScatterX == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisScatterTexel = true;
|
|
}
|
|
#endif
|
|
const FLowResolutionVisibilitySample& LowResScatterSample = LowResolutionVisibilityData(LowResScatterX, LowResScatterY);
|
|
// Only scatter transition distance to mapped texels
|
|
if (LowResScatterSample.IsMapped())
|
|
{
|
|
bool CurrentRegion = false;
|
|
FVector4f ScatterPosition;
|
|
FVector4f ScatterNormal;
|
|
bool bFoundScatterPosition = false;
|
|
|
|
if (LowResScatterSample.NeedsHighResSampling())
|
|
{
|
|
// If the low res scatter sample has high resolution samples, use the center high resolution sample's visibility
|
|
const FVisibilitySample& HighResScatterSample = LowResScatterSample.HighResolutionSamples[(UpsampleFactor / 2) * UpsampleFactor + UpsampleFactor / 2];
|
|
if (HighResScatterSample.IsMapped())
|
|
{
|
|
CurrentRegion = HighResScatterSample.IsVisible();
|
|
ScatterPosition = HighResScatterSample.GetPosition();
|
|
ScatterNormal = HighResScatterSample.GetNormal();
|
|
bFoundScatterPosition = true;
|
|
}
|
|
else
|
|
{
|
|
// If the centered high resolution texel is not mapped,
|
|
// Search all of the high resolution texels corresponding to the low resolution distance field texel for the closest mapped texel.
|
|
float ClosestMappedSubSampleDistSquared = FLT_MAX;
|
|
for (int32 SubY = 0; SubY < UpsampleFactor; SubY++)
|
|
{
|
|
for (int32 SubX = 0; SubX < UpsampleFactor; SubX++)
|
|
{
|
|
const FVisibilitySample& SubHighResSample = LowResScatterSample.HighResolutionSamples[SubY * UpsampleFactor + SubX];
|
|
const float SubSampleDistanceSquared = FMath::Square(SubX - UpsampleFactor / 2) + FMath::Square(SubY - UpsampleFactor / 2);
|
|
if (SubHighResSample.IsMapped() && SubSampleDistanceSquared < ClosestMappedSubSampleDistSquared)
|
|
{
|
|
ClosestMappedSubSampleDistSquared = SubSampleDistanceSquared;
|
|
CurrentRegion = SubHighResSample.IsVisible();
|
|
ScatterPosition = SubHighResSample.GetPosition();
|
|
ScatterNormal = SubHighResSample.GetNormal();
|
|
bFoundScatterPosition = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No high resolution scatter samples were found, use the position and visibility of the low resolution sample
|
|
if (!bFoundScatterPosition)
|
|
{
|
|
CurrentRegion = LowResScatterSample.IsVisible();
|
|
ScatterPosition = LowResScatterSample.GetPosition();
|
|
ScatterNormal = LowResScatterSample.GetNormal();
|
|
}
|
|
|
|
// World space distance from the distance field texel to the nearest shadow transition
|
|
const float TransitionDistance = (ScatterPosition - HighResSample.GetPosition()).Size3();
|
|
const float NormalizedDistance = FMath::Clamp(TransitionDistance / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.0f, 1.0f);
|
|
FSignedDistanceFieldShadowSample& FinalShadowSample = (*ShadowMapData)(LowResScatterX, LowResScatterY);
|
|
// If LowResScatterSample.IsMapped() is true, the distance field texel must be mapped.
|
|
checkSlow(FinalShadowSample.bIsMapped);
|
|
// Only write to distance field texels whose existing transition distance is further than the transition distance being scattered.
|
|
if (NormalizedDistance * .5f < FMath::Abs(FinalShadowSample.Distance - .5f))
|
|
{
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
// Debug when the selected texel is being scattered to
|
|
// This may get hit any number of times, only the last hit will get stored in the distance field
|
|
if (bDebugThisScatterTexel)
|
|
{
|
|
NumScattersToSelectedTexel++;
|
|
}
|
|
#endif
|
|
// Encode the transition distance so that [.5,0] corresponds to [0,1] for shadowed texels, and [.5,1] corresponds to [0,1] for unshadowed texels.
|
|
// .5 of the encoded distance lies exactly on the shadow transition.
|
|
FinalShadowSample.Distance = CurrentRegion ? (NormalizedDistance) * .5f + .5f : .5f - NormalizedDistance * .5f;
|
|
// Approximate the penumbra size using PenumbraSize = (ReceiverDistanceFromLight - OccluderDistanceFromLight) * LightSize / OccluderDistanceFromLight,
|
|
// Which is from the paper "Percentage-Closer Soft Shadows" by Randima Fernando
|
|
const float ReceiverDistanceFromLight = (Light->LightCenterPosition(ScatterPosition, ScatterNormal) - ScatterPosition).Size3();
|
|
// World space distance from center of penumbra to fully shadowed or fully lit transition
|
|
const float PenumbraSize = HighResSample.GetOccluderDistance() * Light->LightSourceRadius / (ReceiverDistanceFromLight - HighResSample.GetOccluderDistance());
|
|
// Normalize the penumbra size so it is a fraction of MaxTransitionDistanceWorldSpace
|
|
FinalShadowSample.PenumbraSize = FMath::Clamp(PenumbraSize / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.01f, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ShadowMaps.Add(Light, ShadowMapData);
|
|
}
|
|
}
|
|
|
|
void FStaticLightingSystem::CalculateDirectSignedDistanceFieldLightingTextureMappingLightSpace(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
TMap<const FLight*, FSignedDistanceFieldShadowMapData2D*>& ShadowMaps,
|
|
const FTexelToVertexMap& TexelToVertexMap,
|
|
const FTexelToCornersMap& TexelToCornersMap,
|
|
bool bDebugThisMapping,
|
|
const FLight* Light) const
|
|
{
|
|
const FBoxSphereBounds3f MeshInfluenceBounds(TextureMapping->Mesh->BoundingBox.ExpandBy(ShadowSettings.MaxTransitionDistanceWorldSpace));
|
|
|
|
if (Light->AffectsBounds(MeshInfluenceBounds))
|
|
{
|
|
const FBoxSphereBounds3f SceneBounds = FBoxSphereBounds3f(AggregateMesh->GetBounds());
|
|
const FDirectionalLight* DirectionalLight = Light->GetDirectionalLight();
|
|
const FSpotLight* SpotLight = Light->GetSpotLight();
|
|
const FPointLight* PointLight = Light->GetPointLight();
|
|
check(DirectionalLight || SpotLight || PointLight);
|
|
|
|
if (DirectionalLight)
|
|
{
|
|
LIGHTINGSTAT(FManualRDTSCTimer FirstPassSourceTimer(MappingContext.Stats.SignedDistanceFieldSourceFirstPassThreadTime));
|
|
FVector4f XAxis, YAxis;
|
|
DirectionalLight->Direction.FindBestAxisVectors3(XAxis, YAxis);
|
|
// Create a coordinate system for the directional light, with the z axis corresponding to the light's direction
|
|
FMatrix44f WorldToLight = FBasisVectorMatrix44f(XAxis, YAxis, DirectionalLight->Direction, FVector4f(0,0,0));
|
|
|
|
const FBox3f LightSpaceImportanceBounds = MeshInfluenceBounds.GetBox().TransformBy(WorldToLight);
|
|
|
|
FStaticShadowDepthMap ShadowDepthMap;
|
|
|
|
int32 ShadowMapSizeX = FMath::TruncToInt(FMath::Max(LightSpaceImportanceBounds.GetExtent().X * 2.0f * 100.0f / ShadowSettings.MaxTransitionDistanceWorldSpace, 4.0f));
|
|
ShadowMapSizeX = ShadowMapSizeX == appTruncErrorCode ? INT_MAX : ShadowMapSizeX;
|
|
int32 ShadowMapSizeY = FMath::TruncToInt(FMath::Max(LightSpaceImportanceBounds.GetExtent().Y * 2.0f * 100.0f / ShadowSettings.MaxTransitionDistanceWorldSpace, 4.0f));
|
|
ShadowMapSizeY = ShadowMapSizeY == appTruncErrorCode ? INT_MAX : ShadowMapSizeY;
|
|
|
|
uint64 ShadowDepthMapMaxSamples = 4194304;
|
|
|
|
// Clamp the number of dominant shadow samples generated if necessary while maintaining aspect ratio
|
|
if ((uint64)ShadowMapSizeX * (uint64)ShadowMapSizeY > (uint64)ShadowDepthMapMaxSamples)
|
|
{
|
|
const float AspectRatio = ShadowMapSizeX / (float)ShadowMapSizeY;
|
|
ShadowMapSizeY = FMath::TruncToInt(FMath::Sqrt(ShadowDepthMapMaxSamples / AspectRatio));
|
|
ShadowMapSizeX = ShadowDepthMapMaxSamples / ShadowMapSizeY;
|
|
}
|
|
|
|
TArray<float> ShadowMap;
|
|
|
|
// Allocate the shadow map
|
|
ShadowMap.Empty(ShadowMapSizeX * ShadowMapSizeY);
|
|
ShadowMap.AddZeroed(ShadowMapSizeX * ShadowMapSizeY);
|
|
const float ShadowMapStart = LightSpaceImportanceBounds.Max.Z - SceneBounds.SphereRadius * 2;
|
|
const FMatrix44f LightToWorld = WorldToLight.InverseFast();
|
|
|
|
for (int32 Y = 0; Y < ShadowMapSizeY; Y++)
|
|
{
|
|
const float YFraction = (Y + .5f) / (float)(ShadowMapSizeY - 1);
|
|
|
|
for (int32 X = 0; X < ShadowMapSizeX; X++)
|
|
{
|
|
const float XFraction = (X + .5f) / (float)(ShadowMapSizeX - 1);
|
|
|
|
const FVector4f LightSpaceEndPosition(
|
|
LightSpaceImportanceBounds.Min.X + XFraction * (LightSpaceImportanceBounds.Max.X - LightSpaceImportanceBounds.Min.X),
|
|
LightSpaceImportanceBounds.Min.Y + YFraction * (LightSpaceImportanceBounds.Max.Y - LightSpaceImportanceBounds.Min.Y),
|
|
LightSpaceImportanceBounds.Max.Z);
|
|
const FVector4f WorldSpaceEndPosition = LightToWorld.TransformPosition(LightSpaceEndPosition);
|
|
|
|
const FVector4f LightSpaceStartPosition(LightSpaceEndPosition.X, LightSpaceEndPosition.Y, ShadowMapStart);
|
|
const FVector4f WorldSpaceStartPosition = LightToWorld.TransformPosition(LightSpaceStartPosition);
|
|
|
|
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, MappingContext.RayCache, Intersection);
|
|
|
|
float MaxSampleDistance = SceneBounds.SphereRadius * 2;
|
|
|
|
if (Intersection.bIntersects)
|
|
{
|
|
MaxSampleDistance = (Intersection.IntersectionVertex.WorldPosition - WorldSpaceStartPosition).Size3();
|
|
}
|
|
|
|
ShadowMap[Y * ShadowMapSizeX + X] = MaxSampleDistance;
|
|
}
|
|
}
|
|
|
|
FirstPassSourceTimer.Stop();
|
|
|
|
LIGHTINGSTAT(FScopedRDTSCTimer SearchTimer(MappingContext.Stats.SignedDistanceFieldSearchThreadTime));
|
|
FSignedDistanceFieldShadowMapData2D* ShadowMapData = new FSignedDistanceFieldShadowMapData2D(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
const int32 TransitionSearchTexelRadiusX = FMath::TruncToInt(ShadowMapSizeX * ShadowSettings.MaxTransitionDistanceWorldSpace / LightSpaceImportanceBounds.GetSize().X);
|
|
const int32 TransitionSearchTexelRadiusY = FMath::TruncToInt(ShadowMapSizeY * ShadowSettings.MaxTransitionDistanceWorldSpace / LightSpaceImportanceBounds.GetSize().Y);
|
|
const float BoundsCellSizeX = (LightSpaceImportanceBounds.Max.X - LightSpaceImportanceBounds.Min.X) / ShadowMapSizeX;
|
|
const float BoundsCellSizeY = (LightSpaceImportanceBounds.Max.Y - LightSpaceImportanceBounds.Min.Y) / ShadowMapSizeY;
|
|
const float DepthBias = FMath::Max(BoundsCellSizeX, BoundsCellSizeY);
|
|
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
|
|
if (TexelToVertex.TotalSampleWeight > 0.0f)
|
|
{
|
|
const FVector4f LightSpacePosition = WorldToLight.TransformPosition(TexelToVertex.WorldPosition);
|
|
const FVector3f LightSpaceNormal = WorldToLight.TransformVector(TexelToVertex.WorldTangentZ);
|
|
const float SinThetaX = LightSpaceNormal.X;
|
|
const float TanThetaX = SinThetaX / FMath::Sqrt(1 - SinThetaX * SinThetaX);
|
|
const float SinThetaY = LightSpaceNormal.Y;
|
|
const float TanThetaY = SinThetaY / FMath::Sqrt(1 - SinThetaY * SinThetaY);
|
|
const float SurfaceDepth = LightSpacePosition.Z - ShadowMapStart;
|
|
|
|
const int32 ShadowMapX = FMath::Clamp(FMath::TruncToInt((LightSpacePosition.X - LightSpaceImportanceBounds.Min.X) / BoundsCellSizeX), 0, ShadowMapSizeX - 1);
|
|
const int32 ShadowMapY = FMath::Clamp(FMath::TruncToInt((LightSpacePosition.Y - LightSpaceImportanceBounds.Min.Y) / BoundsCellSizeY), 0, ShadowMapSizeY - 1);
|
|
|
|
const float TexelShadowMapDepth = ShadowMap[ShadowMapY * ShadowMapSizeX + ShadowMapX];
|
|
const float SlopeScaledDepthBias = 4 * FMath::Max(BoundsCellSizeX * FMath::Abs(TanThetaX), BoundsCellSizeY * FMath::Abs(TanThetaY));
|
|
const bool bTexelVisible = TexelShadowMapDepth > SurfaceDepth - SlopeScaledDepthBias - DepthBias;
|
|
float ClosestTransition = ShadowSettings.MaxTransitionDistanceWorldSpace;
|
|
//float ClosestTransitionPenumbraSize = 1;
|
|
float MostShadowingTransition = 1;
|
|
float MostShadowingTransitionDistance = 1;
|
|
float MostShadowingTransitionPenumbraSize = 1;
|
|
|
|
for (int32 SearchY = FMath::Max(ShadowMapY - TransitionSearchTexelRadiusY, 0); SearchY < FMath::Min(ShadowMapY + TransitionSearchTexelRadiusY, ShadowMapSizeY); SearchY++)
|
|
{
|
|
for (int32 SearchX = FMath::Max(ShadowMapX - TransitionSearchTexelRadiusX, 0); SearchX < FMath::Min(ShadowMapX + TransitionSearchTexelRadiusX, ShadowMapSizeX); SearchX++)
|
|
{
|
|
const FVector2f LightSpaceXYOffset((SearchX - ShadowMapX) * BoundsCellSizeX, (SearchY - ShadowMapY) * BoundsCellSizeY);
|
|
const float PlaneHeightOffsetX = LightSpaceXYOffset.X * TanThetaX;
|
|
const float PlaneHeightOffsetY = LightSpaceXYOffset.Y * TanThetaY;
|
|
const float PlaneHeightOffset = PlaneHeightOffsetX + PlaneHeightOffsetY;
|
|
|
|
const float SearchShadowMapDepth = ShadowMap[SearchY * ShadowMapSizeX + SearchX];
|
|
const float ExtrapolatedSurfaceDepth = SurfaceDepth + PlaneHeightOffset;
|
|
const float SearchTransition = LightSpaceXYOffset.Size();
|
|
const float SameSurfaceDepthBias = SearchTransition * 1.0f;
|
|
const bool bSearchTexelVisible = SearchShadowMapDepth > ExtrapolatedSurfaceDepth - SlopeScaledDepthBias - DepthBias - SameSurfaceDepthBias;
|
|
|
|
if (bTexelVisible)
|
|
{
|
|
const float SearchNormalizedDistance = FMath::Clamp(SearchTransition / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.0f, 1.0f);
|
|
const float SearchEncodedDistance = bTexelVisible ? (SearchNormalizedDistance) * .5f + .5f : .5f - SearchNormalizedDistance * .5f;
|
|
|
|
const float ReceiverDistanceFromLight = SurfaceDepth;
|
|
const float OccluderDistanceFromLight = bTexelVisible ? SearchShadowMapDepth : TexelShadowMapDepth;
|
|
// World space distance from center of penumbra to fully shadowed or fully lit transition
|
|
const float SearchPenumbraSize = (ReceiverDistanceFromLight - OccluderDistanceFromLight) * Light->LightSourceRadius / OccluderDistanceFromLight;
|
|
const float SearchEncodedPenumbraSize = FMath::Clamp(SearchPenumbraSize / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.01f, 1.0f);
|
|
|
|
const float SearchShadowing = FMath::Clamp(SearchEncodedDistance / SearchEncodedPenumbraSize - .5f / SearchEncodedPenumbraSize + .5f, 0.0f, 1.0f);
|
|
|
|
if (bSearchTexelVisible != bTexelVisible
|
|
/* && SearchTransition < ClosestTransition*/
|
|
&& SearchShadowing < MostShadowingTransition)
|
|
{
|
|
MostShadowingTransition = SearchShadowing;
|
|
MostShadowingTransitionDistance = SearchEncodedDistance;
|
|
MostShadowingTransitionPenumbraSize = SearchEncodedPenumbraSize;
|
|
//ClosestTransition = SearchTransition;
|
|
/*
|
|
if (bTexelVisible)
|
|
{
|
|
// Approximate the penumbra size using PenumbraSize = (ReceiverDistanceFromLight - OccluderDistanceFromLight) * LightSize / OccluderDistanceFromLight,
|
|
// Which is from the paper "Percentage-Closer Soft Shadows" by Randima Fernando
|
|
const float ReceiverDistanceFromLight = SurfaceDepth;
|
|
const float OccluderDistanceFromLight = SearchShadowMapDepth;
|
|
// World space distance from center of penumbra to fully shadowed or fully lit transition
|
|
const float PenumbraSize = (ReceiverDistanceFromLight - OccluderDistanceFromLight) * Light->LightSourceRadius / OccluderDistanceFromLight;
|
|
// Normalize the penumbra size so it is a fraction of MaxTransitionDistanceWorldSpace
|
|
ClosestTransitionPenumbraSize = FMath::Clamp(PenumbraSize / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.01f, 1.0f);
|
|
}*/
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bSearchTexelVisible != bTexelVisible
|
|
&& SearchTransition < ClosestTransition)
|
|
{
|
|
ClosestTransition = SearchTransition;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FSignedDistanceFieldShadowSample& FinalShadowSample = (*ShadowMapData)(X, Y);
|
|
FinalShadowSample.bIsMapped = true;
|
|
FinalShadowSample.Distance = MostShadowingTransitionDistance;
|
|
FinalShadowSample.PenumbraSize = MostShadowingTransitionPenumbraSize;
|
|
|
|
if (!bTexelVisible)
|
|
{
|
|
const float NormalizedDistance = FMath::Clamp(ClosestTransition / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.0f, 1.0f);
|
|
// Encode the transition distance so that [.5,0] corresponds to [0,1] for shadowed texels, and [.5,1] corresponds to [0,1] for unshadowed texels.
|
|
// .5 of the encoded distance lies exactly on the shadow transition.
|
|
FinalShadowSample.Distance = bTexelVisible ? (NormalizedDistance) * .5f + .5f : .5f - NormalizedDistance * .5f;
|
|
|
|
const float ReceiverDistanceFromLight = SurfaceDepth;
|
|
const float OccluderDistanceFromLight = TexelShadowMapDepth;
|
|
const float PenumbraSize = (ReceiverDistanceFromLight - OccluderDistanceFromLight) * Light->LightSourceRadius / OccluderDistanceFromLight;
|
|
FinalShadowSample.PenumbraSize = FMath::Clamp(PenumbraSize / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.01f, 1.0f);
|
|
}
|
|
/*
|
|
const float NormalizedDistance = FMath::Clamp(ClosestTransition / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.0f, 1.0f);
|
|
// Encode the transition distance so that [.5,0] corresponds to [0,1] for shadowed texels, and [.5,1] corresponds to [0,1] for unshadowed texels.
|
|
// .5 of the encoded distance lies exactly on the shadow transition.
|
|
FinalShadowSample.Distance = bTexelVisible ? (NormalizedDistance) * .5f + .5f : .5f - NormalizedDistance * .5f;
|
|
|
|
if (bTexelVisible)
|
|
{
|
|
FinalShadowSample.PenumbraSize = ClosestTransitionPenumbraSize;
|
|
}
|
|
else
|
|
{
|
|
const float ReceiverDistanceFromLight = SurfaceDepth;
|
|
const float OccluderDistanceFromLight = TexelShadowMapDepth;
|
|
const float PenumbraSize = (ReceiverDistanceFromLight - OccluderDistanceFromLight) * Light->LightSourceRadius / OccluderDistanceFromLight;
|
|
FinalShadowSample.PenumbraSize = FMath::Clamp(PenumbraSize / ShadowSettings.MaxTransitionDistanceWorldSpace, 0.01f, 1.0f);
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
|
|
ShadowMaps.Add(Light, ShadowMapData);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Estimate direct lighting using the direct photon map.
|
|
* This is only useful for debugging what the final gather rays see.
|
|
*/
|
|
void FStaticLightingSystem::CalculateDirectLightingTextureMappingPhotonMap(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
TMap<const FLight*, FShadowMapData2D*>& ShadowMaps,
|
|
const FTexelToVertexMap& TexelToVertexMap,
|
|
bool bDebugThisMapping) const
|
|
{
|
|
for (int32 LightIndex = 0; LightIndex < TextureMapping->Mesh->RelevantLights.Num(); LightIndex++)
|
|
{
|
|
FLight* Light = TextureMapping->Mesh->RelevantLights[LightIndex];
|
|
if (Light->GetMeshAreaLight() == NULL)
|
|
{
|
|
LightMapData.AddLight(Light);
|
|
}
|
|
}
|
|
|
|
TArray<FIrradiancePhoton*> TempIrradiancePhotons;
|
|
// Calculate direct lighting for each texel.
|
|
for (int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FGatheredLightMapSample& CurrentLightSample = LightMapData(X,Y);
|
|
if (CurrentLightSample.bIsMapped)
|
|
{
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
FStaticLightingVertex CurrentVertex = TexelToVertex.GetVertex();
|
|
|
|
if (PhotonMappingSettings.bUseIrradiancePhotons)
|
|
{
|
|
FLinearColor DirectLighting;
|
|
|
|
const FIrradiancePhoton* NearestPhoton = NULL;
|
|
if (PhotonMappingSettings.bCacheIrradiancePhotonsOnSurfaces)
|
|
{
|
|
// Trace a ray into the current texel to get a good representation of what the final gather will see.
|
|
// Speed does not matter here since bVisualizeCachedApproximateDirectLighting is only used for debugging.
|
|
const FLightRay TexelRay(
|
|
CurrentVertex.WorldPosition + CurrentVertex.WorldTangentZ * TexelToVertex.TexelRadius,
|
|
CurrentVertex.WorldPosition - CurrentVertex.WorldTangentZ * TexelToVertex.TexelRadius,
|
|
TextureMapping,
|
|
NULL
|
|
);
|
|
|
|
FLightRayIntersection Intersection;
|
|
AggregateMesh->IntersectLightRay(TexelRay, true, false, false, MappingContext.RayCache, Intersection);
|
|
|
|
if (Intersection.bIntersects && TextureMapping == Intersection.Mapping)
|
|
{
|
|
CurrentVertex = Intersection.IntersectionVertex;
|
|
}
|
|
else
|
|
{
|
|
// Fall back to using the UV's of this texel
|
|
CurrentVertex.TextureCoordinates[1] = FVector2f(X / (float)TextureMapping->CachedSizeX, Y / (float)TextureMapping->CachedSizeY);
|
|
}
|
|
|
|
// Find the nearest irradiance photon that was cached on this surface
|
|
|
|
checkf(0, TEXT("No longer implemented"));
|
|
}
|
|
else
|
|
{
|
|
// Find the nearest irradiance photon by searching the irradiance photon map
|
|
NearestPhoton = FindNearestIrradiancePhoton(CurrentVertex, MappingContext, TempIrradiancePhotons, false, bDebugThisTexel);
|
|
|
|
FGatheredLightSample DirectLightingSample;
|
|
float Unused2;
|
|
TArray<FVector3f, TInlineAllocator<1>> VertexOffsets;
|
|
VertexOffsets.Add(FVector3f(0, 0, 0));
|
|
|
|
CalculateApproximateDirectLighting(CurrentVertex, TexelToVertex.TexelRadius, VertexOffsets, .1f, true, true, bDebugThisTexel, MappingContext, DirectLightingSample, Unused2);
|
|
|
|
DirectLighting = DirectLightingSample.IncidentLighting;
|
|
}
|
|
const FLinearColor& PhotonIrradiance = NearestPhoton ? NearestPhoton->GetIrradiance() : FLinearColor::Black;
|
|
if (GeneralSettings.ViewSingleBounceNumber < 1)
|
|
{
|
|
FLinearColor FinalLighting = PhotonIrradiance;
|
|
|
|
if (!PhotonMappingSettings.bUsePhotonDirectLightingInFinalGather)
|
|
{
|
|
FinalLighting += DirectLighting;
|
|
}
|
|
|
|
//@todo - can't visualize accurately using AmbientLight with directional lightmaps
|
|
//CurrentLightSample.AddWeighted(FGatheredLightSampleUtil::AmbientLight<2>(FinalLighting), 1.0f);
|
|
CurrentLightSample.AddWeighted(FGatheredLightSampleUtil::PointLightWorldSpace<2>(FinalLighting, FVector4f(0, 0, 1), CurrentVertex.WorldTangentZ), 1.0f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Estimate incident radiance from the photons in the direct photon map
|
|
const FGatheredLightSample PhotonIncidentRadiance = CalculatePhotonIncidentRadiance(DirectPhotonMap, NumPhotonsEmittedDirect, PhotonMappingSettings.DirectPhotonSearchDistance, CurrentVertex, bDebugThisTexel);
|
|
if (GeneralSettings.ViewSingleBounceNumber < 1)
|
|
{
|
|
CurrentLightSample.AddWeighted(PhotonIncidentRadiance, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds an irradiance cache for a given mapping task.
|
|
* This can be called from any thread, not just the thread that owns the mapping, so called code must be thread safe in that manner.
|
|
*/
|
|
void FStaticLightingSystem::ProcessCacheIndirectLightingTask(FCacheIndirectTaskDescription* Task, bool bProcessedByMappingThread)
|
|
{
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
FLMRandomStream SampleGenerator(Task->StartY * Task->TextureMapping->CachedSizeX + Task->StartX);
|
|
|
|
// Calculate incident radiance from indirect lighting
|
|
// With irradiance caching this is just the first pass, the results are added to the cache
|
|
//@todo - use a hierarchical traversal to minimize the number of samples created
|
|
// See "Problems and Solutions: Implementation Details" from the SIGGRAPH 2008 class titled "Practical Global Illumination with Irradiance Caching"
|
|
for (int32 Y = Task->StartY; Y < Task->StartY + Task->SizeY; Y++)
|
|
{
|
|
for (int32 X = Task->StartX; X < Task->StartX + Task->SizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (Task->bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FGatheredLightMapSample& CurrentLightSample = (*Task->LightMapData)(X,Y);
|
|
if (CurrentLightSample.bIsMapped)
|
|
{
|
|
const FTexelToVertex& TexelToVertex = (*Task->TexelToVertexMap)(X,Y);
|
|
checkSlow(TexelToVertex.TotalSampleWeight > 0.0f);
|
|
FFullStaticLightingVertex TexelVertex = TexelToVertex.GetFullVertex();
|
|
TexelVertex.TextureCoordinates[1] = FVector2f(X / (float)Task->TextureMapping->CachedSizeX, Y / (float)Task->TextureMapping->CachedSizeY);
|
|
|
|
// Calculate incoming radiance for the frontface
|
|
FGatheredLightSample IndirectLightingSample = CachePointIncomingRadiance(
|
|
Task->TextureMapping,
|
|
TexelVertex,
|
|
TexelToVertex.ElementIndex,
|
|
TexelToVertex.TexelRadius,
|
|
TexelToVertex.SampleRadius,
|
|
TexelToVertex.bIntersectingSurface,
|
|
Task->MappingContext,
|
|
SampleGenerator,
|
|
bDebugThisTexel);
|
|
|
|
if (Task->TextureMapping->Mesh->UsesTwoSidedLighting(TexelToVertex.ElementIndex))
|
|
{
|
|
TexelVertex.WorldTangentX = -TexelVertex.WorldTangentX;
|
|
TexelVertex.WorldTangentY = -TexelVertex.WorldTangentY;
|
|
TexelVertex.WorldTangentZ = -TexelVertex.WorldTangentZ;
|
|
|
|
// Calculate incoming radiance for the backface
|
|
const FGatheredLightSample BackFaceIndirectLightingSample = CachePointIncomingRadiance(
|
|
Task->TextureMapping,
|
|
TexelVertex,
|
|
TexelToVertex.ElementIndex,
|
|
TexelToVertex.TexelRadius,
|
|
TexelToVertex.SampleRadius,
|
|
TexelToVertex.bIntersectingSurface,
|
|
Task->MappingContext,
|
|
SampleGenerator,
|
|
bDebugThisTexel);
|
|
// Average front and back face incident lighting
|
|
IndirectLightingSample = (BackFaceIndirectLightingSample + IndirectLightingSample) * 0.5f;
|
|
}
|
|
|
|
if (!IrradianceCachingSettings.bAllowIrradianceCaching)
|
|
{
|
|
CurrentLightSample.AddWeighted(IndirectLightingSample, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const float TaskExecutionTime = FPlatformTime::Seconds() - StartTime;
|
|
|
|
if (bProcessedByMappingThread)
|
|
{
|
|
Task->MappingContext.Stats.IndirectLightingCacheTaskThreadTime += TaskExecutionTime;
|
|
}
|
|
else
|
|
{
|
|
Task->MappingContext.Stats.IndirectLightingCacheTaskThreadTimeSeparateTask += TaskExecutionTime;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interpolates from the irradiance cache for a given mapping task.
|
|
* This can be called from any thread, not just the thread that owns the mapping, so called code must be thread safe in that manner.
|
|
*/
|
|
void FStaticLightingSystem::ProcessInterpolateTask(FInterpolateIndirectTaskDescription* Task, bool bProcessedByMappingThread)
|
|
{
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
// Interpolate irradiance cache samples in a separate shading pass
|
|
// This avoids interpolating to positions where more samples will be added later, which would create a discontinuity
|
|
// Also allows us to use more lenient restrictions in this pass, which effectively smooths the irradiance cache results
|
|
for (int32 Y = Task->StartY; Y < Task->StartY + Task->SizeY; Y++)
|
|
{
|
|
for (int32 X = Task->StartX; X < Task->StartX + Task->SizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (Task->bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FGatheredLightMapSample& CurrentLightSample = (*Task->LightMapData)(X,Y);
|
|
if (CurrentLightSample.bIsMapped)
|
|
{
|
|
const FTexelToVertex& TexelToVertex = (*Task->TexelToVertexMap)(X,Y);
|
|
checkSlow(TexelToVertex.TotalSampleWeight > 0.0f);
|
|
FFullStaticLightingVertex TexelVertex = TexelToVertex.GetFullVertex();
|
|
FFinalGatherSample IndirectLighting;
|
|
FFinalGatherSample SecondInterpolatedIndirectLighting;
|
|
float BackfacingHitsFraction = 0.0f;
|
|
float BackFaceBackfacingHitsFraction = 1.0f;
|
|
// Interpolate the indirect lighting from the irradiance cache
|
|
// Interpolation must succeed since this is the second pass
|
|
verify(Task->FirstBounceCache->InterpolateLighting(TexelVertex, false, bDebugThisTexel && GeneralSettings.ViewSingleBounceNumber == 1, IrradianceCachingSettings.SkyOcclusionSmoothnessReduction, IndirectLighting, SecondInterpolatedIndirectLighting, BackfacingHitsFraction, Task->MappingContext.DebugCacheRecords));
|
|
|
|
// Replace sky occlusion in the lighting sample that will be written into the lightmap with the interpolated sky occlusion using IrradianceCachingSettings.SkyOcclusionSmoothnessReduction
|
|
IndirectLighting.SkyOcclusion = SecondInterpolatedIndirectLighting.SkyOcclusion;
|
|
IndirectLighting.StationarySkyLighting = SecondInterpolatedIndirectLighting.StationarySkyLighting;
|
|
|
|
if (Task->TextureMapping->Mesh->UsesTwoSidedLighting(TexelToVertex.ElementIndex))
|
|
{
|
|
TexelVertex.WorldTangentX = -TexelVertex.WorldTangentX;
|
|
TexelVertex.WorldTangentY = -TexelVertex.WorldTangentY;
|
|
TexelVertex.WorldTangentZ = -TexelVertex.WorldTangentZ;
|
|
|
|
FFinalGatherSample BackFaceIndirectLighting;
|
|
FFinalGatherSample BackFaceSecondInterpolatedIndirectLighting;
|
|
// Interpolate indirect lighting for the back face
|
|
verify(Task->FirstBounceCache->InterpolateLighting(TexelVertex, false, bDebugThisTexel && GeneralSettings.ViewSingleBounceNumber == 1, IrradianceCachingSettings.SkyOcclusionSmoothnessReduction, BackFaceIndirectLighting, BackFaceSecondInterpolatedIndirectLighting, BackFaceBackfacingHitsFraction, Task->MappingContext.DebugCacheRecords));
|
|
BackFaceIndirectLighting.SkyOcclusion = BackFaceSecondInterpolatedIndirectLighting.SkyOcclusion;
|
|
// Average front and back face incident lighting
|
|
IndirectLighting = (BackFaceIndirectLighting + IndirectLighting) * 0.5f;
|
|
}
|
|
|
|
if (BackfacingHitsFraction > 0.5f && BackFaceBackfacingHitsFraction > 0.5f)
|
|
{
|
|
CurrentLightSample.bIsMapped = false;
|
|
}
|
|
|
|
float IndirectOcclusion = 1.0f;
|
|
if (AmbientOcclusionSettings.bUseAmbientOcclusion)
|
|
{
|
|
const float DirectOcclusion = 1.0f - AmbientOcclusionSettings.DirectIlluminationOcclusionFraction * IndirectLighting.Occlusion;
|
|
// Apply occlusion to direct lighting, assuming CurrentLightSample only contains direct lighting
|
|
CurrentLightSample.HighQuality = CurrentLightSample.HighQuality * DirectOcclusion;
|
|
CurrentLightSample.LowQuality = CurrentLightSample.LowQuality * DirectOcclusion;
|
|
IndirectOcclusion = 1.0f - AmbientOcclusionSettings.IndirectIlluminationOcclusionFraction * IndirectLighting.Occlusion;
|
|
}
|
|
|
|
IndirectLighting.ApplyOcclusion(IndirectOcclusion);
|
|
|
|
// Apply occlusion to indirect lighting and add this texel's indirect lighting to its running total
|
|
CurrentLightSample.AddWeighted(IndirectLighting, 1);
|
|
CurrentLightSample.HighQuality.AOMaterialMask = IndirectLighting.Occlusion;
|
|
|
|
if (AmbientOcclusionSettings.bUseAmbientOcclusion && AmbientOcclusionSettings.bVisualizeAmbientOcclusion)
|
|
{
|
|
//@todo - this will only be the correct intensity for simple lightmaps
|
|
const FGatheredLightSample OcclusionVisualization = FGatheredLightSampleUtil::AmbientLight<2>(
|
|
FLinearColor(1.0f - IndirectLighting.Occlusion, 1.0f - IndirectLighting.Occlusion, 1.0f - IndirectLighting.Occlusion) * 0.5f);
|
|
// Overwrite the lighting accumulated so far
|
|
CurrentLightSample = OcclusionVisualization;
|
|
CurrentLightSample.bIsMapped = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const float TaskExecutionTime = FPlatformTime::Seconds() - StartTime;
|
|
|
|
if (bProcessedByMappingThread)
|
|
{
|
|
Task->MappingContext.Stats.SecondPassIrradianceCacheInterpolationTime += TaskExecutionTime;
|
|
}
|
|
else
|
|
{
|
|
Task->MappingContext.Stats.SecondPassIrradianceCacheInterpolationTimeSeparateTask += TaskExecutionTime;
|
|
}
|
|
}
|
|
|
|
/** Handles indirect lighting calculations for a single texture mapping. */
|
|
void FStaticLightingSystem::CalculateIndirectLightingTextureMapping(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
const FTexelToVertexMap& TexelToVertexMap,
|
|
bool bDebugThisMapping)
|
|
{
|
|
// Whether to debug the task containing the selected texel only
|
|
const bool bDebugSelectedTaskOnly = true;
|
|
|
|
if (GeneralSettings.NumIndirectLightingBounces > 0 || AmbientOcclusionSettings.bUseAmbientOcclusion || SkyLights.Num() > 0)
|
|
{
|
|
const double StartCacheTime = FPlatformTime::Seconds();
|
|
|
|
const int32 CacheTaskSize = IrradianceCachingSettings.CacheTaskSize;
|
|
int32 NumTasksSubmitted = 0;
|
|
|
|
// Break this mapping into multiple caching tasks in texture space blocks
|
|
for (int32 TaskY = 0; TaskY < TextureMapping->CachedSizeY; TaskY += CacheTaskSize)
|
|
{
|
|
for (int32 TaskX = 0; TaskX < TextureMapping->CachedSizeX; TaskX += CacheTaskSize)
|
|
{
|
|
FCacheIndirectTaskDescription* NewTask = new FCacheIndirectTaskDescription(TextureMapping->Mesh, *this);
|
|
NewTask->StartX = TaskX;
|
|
NewTask->StartY = TaskY;
|
|
NewTask->SizeX = FMath::Min(CacheTaskSize, TextureMapping->CachedSizeX - TaskX);
|
|
NewTask->SizeY = FMath::Min(CacheTaskSize, TextureMapping->CachedSizeY - TaskY);
|
|
NewTask->TextureMapping = TextureMapping;
|
|
NewTask->LightMapData = &LightMapData;
|
|
NewTask->TexelToVertexMap = &TexelToVertexMap;
|
|
|
|
NewTask->bDebugThisMapping = bDebugThisMapping
|
|
&& (!bDebugSelectedTaskOnly
|
|
|| (Scene.DebugInput.LocalX >= TaskX && Scene.DebugInput.LocalX < TaskX + CacheTaskSize
|
|
&& Scene.DebugInput.LocalY >= TaskY && Scene.DebugInput.LocalY < TaskY + CacheTaskSize));
|
|
|
|
NumTasksSubmitted++;
|
|
|
|
// Add to the queue so other lighting threads can pick up these tasks
|
|
FPlatformAtomics::InterlockedIncrement(&TextureMapping->NumOutstandingCacheTasks);
|
|
CacheIndirectLightingTasks.Push(NewTask);
|
|
}
|
|
}
|
|
|
|
do
|
|
{
|
|
// Process caching tasks from any threads until this mapping's tasks are complete
|
|
FCacheIndirectTaskDescription* NextTask = CacheIndirectLightingTasks.Pop();
|
|
|
|
if (NextTask)
|
|
{
|
|
NextTask->bProcessedOnMainThread = true;
|
|
ProcessCacheIndirectLightingTask(NextTask, true);
|
|
// Add to the mapping's queue when complete
|
|
NextTask->TextureMapping->CompletedCacheIndirectLightingTasks.Push(NextTask);
|
|
FPlatformAtomics::InterlockedDecrement(&NextTask->TextureMapping->NumOutstandingCacheTasks);
|
|
}
|
|
}
|
|
while (TextureMapping->NumOutstandingCacheTasks > 0);
|
|
|
|
TArray<FCacheIndirectTaskDescription*> CompletedCILTasks;
|
|
TextureMapping->CompletedCacheIndirectLightingTasks.PopAll(CompletedCILTasks);
|
|
check(CompletedCILTasks.Num() == NumTasksSubmitted);
|
|
|
|
int32 NextRecordId = 0;
|
|
|
|
for (int32 TaskIndex = 0; TaskIndex < CompletedCILTasks.Num(); TaskIndex++)
|
|
{
|
|
FCacheIndirectTaskDescription* Task = CompletedCILTasks[TaskIndex];
|
|
|
|
TArray<TLightingCache<FFinalGatherSample>::FRecord<FFinalGatherSample> > Records;
|
|
Task->MappingContext.FirstBounceCache.GetAllRecords(Records);
|
|
|
|
// Merge the first bounce irradiance caches into one
|
|
for (int32 RecordIndex = 0; RecordIndex < Records.Num(); RecordIndex++)
|
|
{
|
|
Records[RecordIndex].Id += NextRecordId;
|
|
MappingContext.FirstBounceCache.AddRecord(Records[RecordIndex], false, false);
|
|
}
|
|
|
|
for (int32 RecordIndex = 0; RecordIndex < Task->MappingContext.DebugCacheRecords.Num(); RecordIndex++)
|
|
{
|
|
Task->MappingContext.DebugCacheRecords[RecordIndex].RecordId += NextRecordId;
|
|
}
|
|
|
|
MappingContext.DebugCacheRecords.Append(Task->MappingContext.DebugCacheRecords);
|
|
|
|
NextRecordId += Records.Num();
|
|
|
|
// Note: the task's MappingContext stats will be merged into the global stats automatically due to the MappingContext destructor
|
|
|
|
delete CompletedCILTasks[TaskIndex];
|
|
}
|
|
|
|
const double EndCacheTime = FPlatformTime::Seconds();
|
|
|
|
MappingContext.Stats.BlockOnIndirectLightingCacheTasksTime += EndCacheTime - StartCacheTime;
|
|
|
|
if (IrradianceCachingSettings.bAllowIrradianceCaching)
|
|
{
|
|
const int32 InterpolationTaskSize = IrradianceCachingSettings.InterpolateTaskSize;
|
|
int32 NumIILTasksSubmitted = 0;
|
|
|
|
// Break this mapping into multiple interpolation tasks in texture space blocks
|
|
for (int32 TaskY = 0; TaskY < TextureMapping->CachedSizeY; TaskY += InterpolationTaskSize)
|
|
{
|
|
for (int32 TaskX = 0; TaskX < TextureMapping->CachedSizeX; TaskX += InterpolationTaskSize)
|
|
{
|
|
FInterpolateIndirectTaskDescription* NewTask = new FInterpolateIndirectTaskDescription(TextureMapping->Mesh, *this);
|
|
NewTask->StartX = TaskX;
|
|
NewTask->StartY = TaskY;
|
|
NewTask->SizeX = FMath::Min(InterpolationTaskSize, TextureMapping->CachedSizeX - TaskX);
|
|
NewTask->SizeY = FMath::Min(InterpolationTaskSize, TextureMapping->CachedSizeY - TaskY);
|
|
NewTask->TextureMapping = TextureMapping;
|
|
NewTask->LightMapData = &LightMapData;
|
|
NewTask->TexelToVertexMap = &TexelToVertexMap;
|
|
NewTask->FirstBounceCache = &MappingContext.FirstBounceCache;
|
|
NewTask->MappingContext.DebugCacheRecords = MappingContext.DebugCacheRecords;
|
|
|
|
NewTask->bDebugThisMapping = bDebugThisMapping
|
|
&& (!bDebugSelectedTaskOnly
|
|
|| (Scene.DebugInput.LocalX >= TaskX && Scene.DebugInput.LocalX < TaskX + InterpolationTaskSize
|
|
&& Scene.DebugInput.LocalY >= TaskY && Scene.DebugInput.LocalY < TaskY + InterpolationTaskSize));
|
|
|
|
NumIILTasksSubmitted++;
|
|
FPlatformAtomics::InterlockedIncrement(&TextureMapping->NumOutstandingInterpolationTasks);
|
|
InterpolateIndirectLightingTasks.Push(NewTask);
|
|
}
|
|
}
|
|
|
|
do
|
|
{
|
|
FInterpolateIndirectTaskDescription* NextTask = InterpolateIndirectLightingTasks.Pop();
|
|
|
|
if (NextTask)
|
|
{
|
|
ProcessInterpolateTask(NextTask, true);
|
|
NextTask->TextureMapping->CompletedInterpolationTasks.Push(NextTask);
|
|
FPlatformAtomics::InterlockedDecrement(&NextTask->TextureMapping->NumOutstandingInterpolationTasks);
|
|
}
|
|
}
|
|
while (TextureMapping->NumOutstandingInterpolationTasks > 0);
|
|
|
|
TArray<FInterpolateIndirectTaskDescription*> CompletedTasks;
|
|
TextureMapping->CompletedInterpolationTasks.PopAll(CompletedTasks);
|
|
check(CompletedTasks.Num() == NumIILTasksSubmitted);
|
|
|
|
for (int32 TaskIndex = 0; TaskIndex < CompletedTasks.Num(); TaskIndex++)
|
|
{
|
|
FInterpolateIndirectTaskDescription* Task = CompletedTasks[TaskIndex];
|
|
|
|
check(Task->MappingContext.DebugCacheRecords.Num() == MappingContext.DebugCacheRecords.Num());
|
|
|
|
for (int32 CacheRecordIndex = 0; CacheRecordIndex < MappingContext.DebugCacheRecords.Num(); CacheRecordIndex++)
|
|
{
|
|
// Combine results
|
|
MappingContext.DebugCacheRecords[CacheRecordIndex].bAffectsSelectedTexel |= Task->MappingContext.DebugCacheRecords[CacheRecordIndex].bAffectsSelectedTexel;
|
|
}
|
|
|
|
delete CompletedTasks[TaskIndex];
|
|
}
|
|
|
|
MappingContext.DebugOutput->CacheRecords = MappingContext.DebugCacheRecords;
|
|
}
|
|
|
|
MappingContext.Stats.BlockOnIndirectLightingInterpolateTasksTime += FPlatformTime::Seconds() - EndCacheTime;
|
|
}
|
|
|
|
FPlatformAtomics::InterlockedDecrement(&TasksInProgressThatWillNeedHelp);
|
|
}
|
|
|
|
/** Overrides LightMapData with material attributes if MaterialSettings.ViewMaterialAttribute != VMA_None */
|
|
void FStaticLightingSystem::ViewMaterialAttributesTextureMapping(
|
|
FStaticLightingTextureMapping* TextureMapping,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
const FTexelToVertexMap& TexelToVertexMap,
|
|
bool bDebugThisMapping) const
|
|
{
|
|
if (MaterialSettings.ViewMaterialAttribute != VMA_None)
|
|
{
|
|
for(int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
for(int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FGatheredLightMapSample& CurrentLightSample = LightMapData(X,Y);
|
|
if (CurrentLightSample.bIsMapped)
|
|
{
|
|
const FTexelToVertex& TexelToVertex = TexelToVertexMap(X,Y);
|
|
checkSlow(TexelToVertex.TotalSampleWeight > 0.0f);
|
|
FStaticLightingVertex CurrentVertex = TexelToVertex.GetVertex();
|
|
|
|
// Trace a ray into the current texel to get a good representation of what material lookups from ray intersections will see.
|
|
// Speed does not matter here since this visualization is only used for debugging.
|
|
const FLightRay TexelRay(
|
|
CurrentVertex.WorldPosition + CurrentVertex.WorldTangentZ * TexelToVertex.TexelRadius,
|
|
CurrentVertex.WorldPosition - CurrentVertex.WorldTangentZ * TexelToVertex.TexelRadius,
|
|
TextureMapping,
|
|
NULL
|
|
);
|
|
|
|
FLightRayIntersection Intersection;
|
|
AggregateMesh->IntersectLightRay(TexelRay, true, true, false, MappingContext.RayCache, Intersection);
|
|
CurrentLightSample = GetVisualizedMaterialAttribute(TextureMapping, Intersection);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** A map from texel to the number of triangles mapped to that texel. */
|
|
class FTexelToNumTrianglesMap
|
|
{
|
|
public:
|
|
|
|
/** Stores information about a texel needed for determining the validity of the lightmap UVs. */
|
|
struct FTexelToNumTriangles
|
|
{
|
|
bool bWrappingUVs;
|
|
int32 NumTriangles;
|
|
};
|
|
|
|
/** Initialization constructor. */
|
|
FTexelToNumTrianglesMap(int32 InSizeX, int32 InSizeY) :
|
|
SizeX(InSizeX),
|
|
SizeY(InSizeY)
|
|
{
|
|
// Clear the map to zero.
|
|
Data.AddZeroed(SizeX * SizeY);
|
|
}
|
|
|
|
// Accessors.
|
|
FTexelToNumTriangles& operator()(int32 X, int32 Y)
|
|
{
|
|
const uint32 TexelIndex = Y * SizeX + X;
|
|
return Data[TexelIndex];
|
|
}
|
|
const FTexelToNumTriangles& operator()(int32 X, int32 Y) const
|
|
{
|
|
const int32 TexelIndex = Y * SizeX + X;
|
|
return Data[TexelIndex];
|
|
}
|
|
|
|
int32 GetSizeX() const { return SizeX; }
|
|
int32 GetSizeY() const { return SizeY; }
|
|
|
|
private:
|
|
|
|
/** The mapping data. */
|
|
TArray<FTexelToNumTriangles> Data;
|
|
|
|
/** The width of the mapping data. */
|
|
int32 SizeX;
|
|
|
|
/** The height of the mapping data. */
|
|
int32 SizeY;
|
|
};
|
|
|
|
/** Rasterization policy for verifying unique lightmap UVs. */
|
|
class FUniqueMappingRasterPolicy
|
|
{
|
|
public:
|
|
|
|
typedef int32 InterpolantType;
|
|
|
|
/** Initialization constructor. */
|
|
FUniqueMappingRasterPolicy(
|
|
const FScene& InScene,
|
|
FTexelToNumTrianglesMap& InTexelToNumTrianglesMap,
|
|
bool bInDebugThisMapping
|
|
) :
|
|
Scene(InScene),
|
|
TexelToNumTrianglesMap(InTexelToNumTrianglesMap),
|
|
TotalPixelsWritten(0),
|
|
TotalPixelOverlapsOccured(0),
|
|
bDebugThisMapping(bInDebugThisMapping)
|
|
{}
|
|
|
|
int32 GetTotalPixelsWritten() const
|
|
{
|
|
return TotalPixelsWritten;
|
|
}
|
|
int32 GetTotalPixelOverlapsOccured() const
|
|
{
|
|
return TotalPixelOverlapsOccured;
|
|
}
|
|
protected:
|
|
|
|
// FTriangleRasterizer policy interface.
|
|
int32 GetMinX() const { return 0; }
|
|
int32 GetMaxX() const { return TexelToNumTrianglesMap.GetSizeX() - 1; }
|
|
int32 GetMinY() const { return 0; }
|
|
int32 GetMaxY() const { return TexelToNumTrianglesMap.GetSizeY() - 1; }
|
|
|
|
void ProcessPixel(int32 X, int32 Y, const InterpolantType& Interpolant, bool BackFacing);
|
|
|
|
private:
|
|
|
|
const FScene& Scene;
|
|
|
|
/** The texel to vertex map which is being rasterized to. */
|
|
FTexelToNumTrianglesMap& TexelToNumTrianglesMap;
|
|
|
|
int32 TotalPixelsWritten;
|
|
int32 TotalPixelOverlapsOccured;
|
|
|
|
const bool bDebugThisMapping;
|
|
};
|
|
|
|
void FUniqueMappingRasterPolicy::ProcessPixel(int32 X, int32 Y, const InterpolantType& bWrappingUVs, bool BackFacing)
|
|
{
|
|
FTexelToNumTrianglesMap::FTexelToNumTriangles& TexelToNumTriangles = TexelToNumTrianglesMap(X, Y);
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& X == Scene.DebugInput.LocalX
|
|
&& Y == Scene.DebugInput.LocalY)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
TexelToNumTriangles.NumTriangles++;
|
|
if (TexelToNumTriangles.NumTriangles > 1)
|
|
{
|
|
TotalPixelOverlapsOccured++;
|
|
}
|
|
TotalPixelsWritten++;
|
|
TexelToNumTriangles.bWrappingUVs = !!bWrappingUVs;
|
|
}
|
|
|
|
/** Colors texels with invalid lightmap UVs to make it obvious that they are wrong. */
|
|
void FStaticLightingSystem::ColorInvalidLightmapUVs(
|
|
const FStaticLightingTextureMapping* TextureMapping,
|
|
FGatheredLightMapData2D& LightMapData,
|
|
bool bDebugThisMapping) const
|
|
{
|
|
FTexelToNumTrianglesMap TexelToNumTrianglesMap(TextureMapping->CachedSizeX, TextureMapping->CachedSizeY);
|
|
|
|
// Rasterize the triangle using the mapping's texture coordinate channel.
|
|
FTriangleRasterizer<FUniqueMappingRasterPolicy> TexelMappingRasterizer(FUniqueMappingRasterPolicy(
|
|
Scene,
|
|
TexelToNumTrianglesMap,
|
|
bDebugThisMapping
|
|
));
|
|
|
|
const int32 TriangleCount = TextureMapping->Mesh->NumTriangles;
|
|
// Rasterize the triangles
|
|
for (int32 TriangleIndex = 0; TriangleIndex < TriangleCount; TriangleIndex++)
|
|
{
|
|
// Query the mesh for the triangle's vertices.
|
|
FStaticLightingVertex V0;
|
|
FStaticLightingVertex V1;
|
|
FStaticLightingVertex V2;
|
|
int32 DummyElement;
|
|
TextureMapping->Mesh->GetTriangle(TriangleIndex,V0,V1,V2,DummyElement);
|
|
|
|
const FVector2f UV0 = V0.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex];
|
|
const FVector2f UV1 = V1.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex];
|
|
const FVector2f UV2 = V2.TextureCoordinates[TextureMapping->LightmapTextureCoordinateIndex];
|
|
|
|
bool bHasWrappingLightmapUVs = false;
|
|
//@todo - remove the thresholds and fixup existing content
|
|
if (UV0.X < -DELTA || UV0.X >= 1.0f + DELTA
|
|
|| UV0.Y < -DELTA || UV0.Y >= 1.0f + DELTA
|
|
|| UV1.X < -DELTA || UV1.X >= 1.0f + DELTA
|
|
|| UV1.Y < -DELTA || UV1.Y >= 1.0f + DELTA
|
|
|| UV2.X < -DELTA || UV2.X >= 1.0f + DELTA
|
|
|| UV2.Y < -DELTA || UV2.Y >= 1.0f + DELTA)
|
|
{
|
|
bHasWrappingLightmapUVs = true;
|
|
}
|
|
|
|
// Only rasterize the center of the texel
|
|
TexelMappingRasterizer.DrawTriangle(
|
|
bHasWrappingLightmapUVs,
|
|
bHasWrappingLightmapUVs,
|
|
bHasWrappingLightmapUVs,
|
|
UV0 * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY) + FVector2f(-0.5f,-0.5f),
|
|
UV1 * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY) + FVector2f(-0.5f,-0.5f),
|
|
UV2 * FVector2f(TextureMapping->CachedSizeX,TextureMapping->CachedSizeY) + FVector2f(-0.5f,-0.5f),
|
|
false
|
|
);
|
|
}
|
|
|
|
bool bHasWrappingUVs = false;
|
|
bool bHasOverlappedUVs = false;
|
|
for(int32 Y = 0; Y < TextureMapping->CachedSizeY; Y++)
|
|
{
|
|
// Color texels belonging to vertices with wrapping lightmap UV's bright green
|
|
// Color texels that have more than one triangle mapped to them bright orange
|
|
for(int32 X = 0; X < TextureMapping->CachedSizeX; X++)
|
|
{
|
|
bool bDebugThisTexel = false;
|
|
#if ALLOW_LIGHTMAP_SAMPLE_DEBUGGING
|
|
if (bDebugThisMapping
|
|
&& Y == Scene.DebugInput.LocalY
|
|
&& X == Scene.DebugInput.LocalX)
|
|
{
|
|
bDebugThisTexel = true;
|
|
}
|
|
#endif
|
|
FGatheredLightMapSample& CurrentLightSample = LightMapData(X,Y);
|
|
if (CurrentLightSample.bIsMapped)
|
|
{
|
|
const FTexelToNumTrianglesMap::FTexelToNumTriangles& TexelToNumTriangles = TexelToNumTrianglesMap(X,Y);
|
|
if (TexelToNumTriangles.bWrappingUVs)
|
|
{
|
|
bHasWrappingUVs = true;
|
|
if (Scene.GeneralSettings.bUseErrorColoring && MaterialSettings.ViewMaterialAttribute == VMA_None)
|
|
{
|
|
// Color texels belonging to vertices with wrapping lightmap UV's bright green
|
|
if (TextureMapping->Mesh->ShouldColorInvalidTexels())
|
|
{
|
|
CurrentLightSample = FGatheredLightSampleUtil::AmbientLight<2>(FLinearColor(0.5f, 2.0f, 0.0f));
|
|
CurrentLightSample.bIsMapped = true;
|
|
}
|
|
}
|
|
}
|
|
else if (TexelToNumTriangles.NumTriangles > 1)
|
|
{
|
|
bHasOverlappedUVs = true;
|
|
if (Scene.GeneralSettings.bUseErrorColoring && MaterialSettings.ViewMaterialAttribute == VMA_None)
|
|
{
|
|
// Color texels that have more than one triangle mapped to them bright orange
|
|
if (TextureMapping->Mesh->ShouldColorInvalidTexels())
|
|
{
|
|
CurrentLightSample = FGatheredLightSampleUtil::AmbientLight<2>(FLinearColor(2.0f, 0.7f, 0.0f));
|
|
CurrentLightSample.bIsMapped = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const float OverlapThreshold = 1.0f / 100.0f;
|
|
float NormalizedOverlap = (float)TexelMappingRasterizer.GetTotalPixelOverlapsOccured() / (float)TexelMappingRasterizer.GetTotalPixelsWritten();
|
|
if (bHasWrappingUVs || bHasOverlappedUVs)
|
|
{
|
|
int32 TypeId = TextureMapping->Mesh->GetObjectType();
|
|
FGuid ObjectGuid = TextureMapping->Mesh->GetObjectGuid();
|
|
if (bHasWrappingUVs)
|
|
{
|
|
GSwarm->SendAlertMessage(NSwarm::ALERT_LEVEL_WARNING, ObjectGuid, TypeId, TEXT("LightmassError_ObjectWrappedUVs"));
|
|
}
|
|
if (bHasOverlappedUVs && NormalizedOverlap > OverlapThreshold)
|
|
{
|
|
GSwarm->SendAlertMessage(NSwarm::ALERT_LEVEL_WARNING, ObjectGuid, TypeId, TEXT("LightmassError_ObjectOverlappedUVs"));
|
|
FString Info = FString::Printf(TEXT("Lightmap UV are overlapping by %0.1f%%. Please adjust content - Enable Error Coloring to visualize."), NormalizedOverlap * 100.0f);
|
|
GSwarm->SendAlertMessage(NSwarm::ALERT_LEVEL_INFO, ObjectGuid, TypeId, Info.GetCharArray().GetData());
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Adds a texel of padding around texture mappings and copies the nearest texel into the padding. */
|
|
void FStaticLightingSystem::PadTextureMapping(
|
|
const FStaticLightingTextureMapping* TextureMapping,
|
|
const FGatheredLightMapData2D& LightMapData,
|
|
FGatheredLightMapData2D& PaddedLightMapData,
|
|
TMap<const FLight*, FShadowMapData2D*>& ShadowMaps,
|
|
TMap<const FLight*, FSignedDistanceFieldShadowMapData2D*>& SignedDistanceFieldShadowMaps) const
|
|
{
|
|
if (TextureMapping->bPadded)
|
|
{
|
|
check(TextureMapping->SizeX == TextureMapping->CachedSizeX + 2);
|
|
check(TextureMapping->SizeY == TextureMapping->CachedSizeY + 2);
|
|
// We need to expand it back out...
|
|
uint32 TrueSizeX = TextureMapping->SizeX;
|
|
uint32 TrueSizeY = TextureMapping->SizeY;
|
|
FGatheredLightMapSample DebugLightSample = FGatheredLightSampleUtil::AmbientLight<2>(FLinearColor(1.0f, 0.0f, 1.0f));
|
|
for (uint32 CopyY = 0; CopyY < TrueSizeY; CopyY++)
|
|
{
|
|
if (CopyY == 0)
|
|
{
|
|
// The first row, left corner
|
|
PaddedLightMapData(0,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(0,0);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
PaddedLightMapData(TempX+1,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(TempX,0);
|
|
}
|
|
// The right corner
|
|
PaddedLightMapData(TrueSizeX-1,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(TextureMapping->CachedSizeX-1,0);
|
|
}
|
|
else if (CopyY == TrueSizeY - 1)
|
|
{
|
|
// The last row, left corner
|
|
PaddedLightMapData(0,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(0,TextureMapping->CachedSizeY-1);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
PaddedLightMapData(TempX+1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(TempX,TextureMapping->CachedSizeY-1);
|
|
}
|
|
// The right corner
|
|
PaddedLightMapData(TrueSizeX-1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(TextureMapping->CachedSizeX-1,TextureMapping->CachedSizeY-1);
|
|
}
|
|
else
|
|
{
|
|
// The last row, left corner
|
|
PaddedLightMapData(0,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(0,CopyY-1);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
PaddedLightMapData(TempX+1,CopyY) =LightMapData(TempX,CopyY-1);
|
|
}
|
|
// The right corner
|
|
PaddedLightMapData(TrueSizeX-1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugLightSample : LightMapData(TextureMapping->CachedSizeX-1,CopyY-1);
|
|
}
|
|
}
|
|
PaddedLightMapData.Lights = LightMapData.Lights;
|
|
PaddedLightMapData.bHasSkyShadowing = LightMapData.bHasSkyShadowing;
|
|
|
|
FShadowSample DebugShadowSample;
|
|
DebugShadowSample.bIsMapped = true;
|
|
DebugShadowSample.Visibility = 0.7f;
|
|
for (TMap<const FLight*, FShadowMapData2D*>::TIterator It(ShadowMaps); It; ++It)
|
|
{
|
|
const FLight* Key = It.Key();
|
|
FShadowMapData2D* ShadowMapData = It.Value();
|
|
FShadowMapData2D* TempShadowMapData = new FShadowMapData2D(TrueSizeX, TrueSizeY);
|
|
|
|
// Expand it
|
|
for (uint32 CopyY = 0; CopyY < TrueSizeY; CopyY++)
|
|
{
|
|
if (CopyY == 0)
|
|
{
|
|
// The first row, left corner
|
|
(*TempShadowMapData)(0,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(0,0);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
(*TempShadowMapData)(TempX+1,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(TempX,0) * 2.0f - (*ShadowMapData)(TempX,1);
|
|
}
|
|
// The right corner
|
|
(*TempShadowMapData)(TrueSizeX-1,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(TextureMapping->CachedSizeX-1,0);
|
|
}
|
|
else if (CopyY == TrueSizeY - 1)
|
|
{
|
|
// The last row, left corner
|
|
(*TempShadowMapData)(0,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(0,TextureMapping->CachedSizeY-1);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
(*TempShadowMapData)(TempX+1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(TempX,TextureMapping->CachedSizeY-1) * 2.0f - (*ShadowMapData)(TempX,TextureMapping->CachedSizeY-2);
|
|
}
|
|
// The right corner
|
|
(*TempShadowMapData)(TrueSizeX-1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(TextureMapping->CachedSizeX-1,TextureMapping->CachedSizeY-1);
|
|
}
|
|
else
|
|
{
|
|
// The last row, left corner
|
|
(*TempShadowMapData)(0,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(0,CopyY-1) * 2.0f - (*ShadowMapData)(1,CopyY-1);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
(*TempShadowMapData)(TempX+1,CopyY) = (*ShadowMapData)(TempX,CopyY-1);
|
|
}
|
|
// The right corner
|
|
(*TempShadowMapData)(TrueSizeX-1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugShadowSample : (*ShadowMapData)(TextureMapping->CachedSizeX-1,CopyY-1) * 2.0f - (*ShadowMapData)(TextureMapping->CachedSizeX-2,CopyY-1);
|
|
}
|
|
}
|
|
|
|
// Copy it back in
|
|
ShadowMaps.Add(Key, TempShadowMapData);
|
|
delete ShadowMapData;
|
|
}
|
|
|
|
|
|
FSignedDistanceFieldShadowSample DebugDistanceShadowSample;
|
|
DebugDistanceShadowSample.bIsMapped = true;
|
|
DebugDistanceShadowSample.Distance = .5f;
|
|
for (TMap<const FLight*, FSignedDistanceFieldShadowMapData2D*>::TIterator It(SignedDistanceFieldShadowMaps); It; ++It)
|
|
{
|
|
const FLight* Key = It.Key();
|
|
FSignedDistanceFieldShadowMapData2D* ShadowMapData = It.Value();
|
|
FSignedDistanceFieldShadowMapData2D* TempShadowMapData = new FSignedDistanceFieldShadowMapData2D(TrueSizeX, TrueSizeY);
|
|
|
|
// Expand it
|
|
for (uint32 CopyY = 0; CopyY < TrueSizeY; CopyY++)
|
|
{
|
|
if (CopyY == 0)
|
|
{
|
|
// The first row, left corner
|
|
(*TempShadowMapData)(0,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(0,0);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
// Extrapolate the padding texels, maintaining the same slope that the source data had, which is important for distance field shadows
|
|
(*TempShadowMapData)(TempX+1,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(TempX,0) * 2.0f - (*ShadowMapData)(TempX,1);
|
|
}
|
|
// The right corner
|
|
(*TempShadowMapData)(TrueSizeX-1,0) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(TextureMapping->CachedSizeX-1,0);
|
|
}
|
|
else if (CopyY == TrueSizeY - 1)
|
|
{
|
|
// The last row, left corner
|
|
(*TempShadowMapData)(0,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(0,TextureMapping->CachedSizeY-1);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
(*TempShadowMapData)(TempX+1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(TempX,TextureMapping->CachedSizeY-1) * 2.0f - (*ShadowMapData)(TempX,TextureMapping->CachedSizeY-2);
|
|
}
|
|
// The right corner
|
|
(*TempShadowMapData)(TrueSizeX-1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(TextureMapping->CachedSizeX-1,TextureMapping->CachedSizeY-1);
|
|
}
|
|
else
|
|
{
|
|
// The last row, left corner
|
|
(*TempShadowMapData)(0,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(0,CopyY-1) * 2.0f - (*ShadowMapData)(1,CopyY-1);
|
|
// The rest of the row, short of the right corner
|
|
for (uint32 TempX = 0; TempX < (uint32)(TextureMapping->CachedSizeX); TempX++)
|
|
{
|
|
(*TempShadowMapData)(TempX+1,CopyY) = (*ShadowMapData)(TempX,CopyY-1);
|
|
}
|
|
// The right corner
|
|
(*TempShadowMapData)(TrueSizeX-1,CopyY) = FStaticLightingMapping::s_bShowLightmapBorders ? DebugDistanceShadowSample : (*ShadowMapData)(TextureMapping->CachedSizeX-1,CopyY-1) * 2.0f - (*ShadowMapData)(TextureMapping->CachedSizeX-2,CopyY-1);
|
|
}
|
|
}
|
|
|
|
// Copy it back in
|
|
SignedDistanceFieldShadowMaps.Add(Key, TempShadowMapData);
|
|
delete ShadowMapData;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PaddedLightMapData = LightMapData;
|
|
}
|
|
}
|
|
|
|
/** Rasterizes Mesh into TexelToCornersMap */
|
|
void FStaticLightingSystem::CalculateTexelCorners(const FStaticLightingMesh* Mesh, FTexelToCornersMap& TexelToCornersMap, int32 UVIndex, bool bDebugThisMapping) const
|
|
{
|
|
static const FVector2f CornerOffsets[NumTexelCorners] =
|
|
{
|
|
FVector2f(0, 0),
|
|
FVector2f(-1, 0),
|
|
FVector2f(0, -1),
|
|
FVector2f(-1, -1)
|
|
};
|
|
|
|
// Rasterize each triangle of the mesh
|
|
for (int32 TriangleIndex = 0; TriangleIndex < Mesh->NumTriangles; TriangleIndex++)
|
|
{
|
|
// Query the mesh for the triangle's vertices.
|
|
FStaticLightingVertex V0;
|
|
FStaticLightingVertex V1;
|
|
FStaticLightingVertex V2;
|
|
int32 TriangleElement;
|
|
Mesh->GetTriangle(TriangleIndex, V0, V1, V2, TriangleElement);
|
|
|
|
// Rasterize each triangle offset by the corner offsets
|
|
for (int32 CornerIndex = 0; CornerIndex < NumTexelCorners; CornerIndex++)
|
|
{
|
|
FTriangleRasterizer<FTexelCornerRasterPolicy> TexelCornerRasterizer(FTexelCornerRasterPolicy(
|
|
Scene,
|
|
TexelToCornersMap,
|
|
CornerIndex,
|
|
bDebugThisMapping
|
|
));
|
|
|
|
TexelCornerRasterizer.DrawTriangle(
|
|
V0,
|
|
V1,
|
|
V2,
|
|
V0.TextureCoordinates[UVIndex] * FVector2f(TexelToCornersMap.GetSizeX(), TexelToCornersMap.GetSizeY()) + CornerOffsets[CornerIndex],
|
|
V1.TextureCoordinates[UVIndex] * FVector2f(TexelToCornersMap.GetSizeX(), TexelToCornersMap.GetSizeY()) + CornerOffsets[CornerIndex],
|
|
V2.TextureCoordinates[UVIndex] * FVector2f(TexelToCornersMap.GetSizeX(), TexelToCornersMap.GetSizeY()) + CornerOffsets[CornerIndex],
|
|
false
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Rasterizes Mesh into TexelToCornersMap, with extra parameters like which material index to rasterize and UV scale and bias. */
|
|
void FStaticLightingSystem::CalculateTexelCorners(
|
|
const TArray<int32>& TriangleIndices,
|
|
const TArray<FStaticLightingVertex>& Vertices,
|
|
FTexelToCornersMap& TexelToCornersMap,
|
|
const TArray<int32>& ElementIndices,
|
|
int32 MaterialIndex,
|
|
int32 UVIndex,
|
|
bool bDebugThisMapping,
|
|
FVector2f UVBias,
|
|
FVector2f UVScale) const
|
|
{
|
|
static const FVector2f CornerOffsets[NumTexelCorners] =
|
|
{
|
|
FVector2f(0, 0),
|
|
FVector2f(-1, 0),
|
|
FVector2f(0, -1),
|
|
FVector2f(-1, -1)
|
|
};
|
|
|
|
// Rasterize each triangle of the mesh
|
|
for (int32 TriangleIndex = 0; TriangleIndex < TriangleIndices.Num(); TriangleIndex++)
|
|
{
|
|
if (ElementIndices[TriangleIndices[TriangleIndex]] == MaterialIndex)
|
|
{
|
|
const FStaticLightingVertex& V0 = Vertices[TriangleIndices[TriangleIndex] * 3 + 0];
|
|
const FStaticLightingVertex& V1 = Vertices[TriangleIndices[TriangleIndex] * 3 + 1];
|
|
const FStaticLightingVertex& V2 = Vertices[TriangleIndices[TriangleIndex] * 3 + 2];
|
|
|
|
// Rasterize each triangle offset by the corner offsets
|
|
for (int32 CornerIndex = 0; CornerIndex < NumTexelCorners; CornerIndex++)
|
|
{
|
|
FTriangleRasterizer<FTexelCornerRasterPolicy> TexelCornerRasterizer(FTexelCornerRasterPolicy(
|
|
Scene,
|
|
TexelToCornersMap,
|
|
CornerIndex,
|
|
bDebugThisMapping
|
|
));
|
|
|
|
TexelCornerRasterizer.DrawTriangle(
|
|
V0,
|
|
V1,
|
|
V2,
|
|
UVScale * (UVBias + V0.TextureCoordinates[UVIndex]) * FVector2f(TexelToCornersMap.GetSizeX(), TexelToCornersMap.GetSizeY()) + CornerOffsets[CornerIndex],
|
|
UVScale * (UVBias + V1.TextureCoordinates[UVIndex]) * FVector2f(TexelToCornersMap.GetSizeX(), TexelToCornersMap.GetSizeY()) + CornerOffsets[CornerIndex],
|
|
UVScale * (UVBias + V2.TextureCoordinates[UVIndex]) * FVector2f(TexelToCornersMap.GetSizeX(), TexelToCornersMap.GetSizeY()) + CornerOffsets[CornerIndex],
|
|
false
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FLinearColor FStaticLightingMapping::GetCachedRadiosity(int32 RadiosityBufferIndex, int32 SurfaceCacheIndex) const
|
|
{
|
|
return RadiositySurfaceCache[RadiosityBufferIndex][SurfaceCacheIndex];
|
|
}
|
|
|
|
FLinearColor FStaticLightingTextureMapping::GetSurfaceCacheLighting(const FMinimalStaticLightingVertex& Vertex) const
|
|
{
|
|
checkSlow(SurfaceCacheSizeX > 0 && SurfaceCacheSizeY > 0);
|
|
// Clamping is necessary since the UV's may be outside the [0, 1) range
|
|
const int32 SurfaceCacheX = FMath::Clamp(FMath::TruncToInt(Vertex.TextureCoordinates[1].X * SurfaceCacheSizeX), 0, SurfaceCacheSizeX - 1);
|
|
const int32 SurfaceCacheY = FMath::Clamp(FMath::TruncToInt(Vertex.TextureCoordinates[1].Y * SurfaceCacheSizeY), 0, SurfaceCacheSizeY - 1);
|
|
const int32 SurfaceCacheIndex = SurfaceCacheY * SurfaceCacheSizeX + SurfaceCacheX;
|
|
|
|
FLinearColor Lighting = SurfaceCacheLighting[SurfaceCacheIndex];
|
|
return Lighting;
|
|
}
|
|
|
|
int32 FStaticLightingTextureMapping::GetSurfaceCacheIndex(const struct FMinimalStaticLightingVertex& Vertex) const
|
|
{
|
|
checkSlow(SurfaceCacheSizeX > 0 && SurfaceCacheSizeY > 0);
|
|
// Clamping is necessary since the UV's may be outside the [0, 1) range
|
|
const int32 SurfaceCacheX = FMath::Clamp(FMath::TruncToInt(Vertex.TextureCoordinates[1].X * SurfaceCacheSizeX), 0, SurfaceCacheSizeX - 1);
|
|
const int32 SurfaceCacheY = FMath::Clamp(FMath::TruncToInt(Vertex.TextureCoordinates[1].Y * SurfaceCacheSizeY), 0, SurfaceCacheSizeY - 1);
|
|
return SurfaceCacheY * SurfaceCacheSizeX + SurfaceCacheX;
|
|
}
|
|
|
|
}
|