LumenScreenProbeTracing.usf
[numthreads( 64 , 1, 1)]
void ScreenProbeTraceMeshSDFsCS(
uint3 GroupId : SV_GroupID,
uint3 DispatchThreadId : SV_DispatchThreadID,
uint3 GroupThreadId : SV_GroupThreadID)
{
uint AllocatorIndex = 0 ? 1 : 0;
if (DispatchThreadId.x < CompactedTraceTexelAllocator[AllocatorIndex])
{
uint ScreenProbeIndex;
uint2 TraceTexelCoord;
float TraceHitDistance;
ReadTraceTexel(DispatchThreadId.x, ScreenProbeIndex, TraceTexelCoord, TraceHitDistance);
uint2 ScreenProbeAtlasCoord = uint2(ScreenProbeIndex % ScreenProbeAtlasViewSize.x, ScreenProbeIndex / ScreenProbeAtlasViewSize.x);
TraceMeshSDFs(ScreenProbeAtlasCoord, TraceTexelCoord, ScreenProbeIndex, TraceHitDistance);
}
}
void ReadTraceTexel(uint DispatchThreadId, out uint ScreenProbeIndex, out uint2 TraceTexelCoord, out float TraceHitDistance)
{
DecodeTraceTexel(CompactedTraceTexelData[DispatchThreadId], ScreenProbeIndex, TraceTexelCoord, TraceHitDistance);
}
void DecodeTraceTexel(uint2 TraceTexelData, inout uint ScreenProbeIndex, inout uint2 TraceTexelCoord, inout float TraceHitDistance)
{
ScreenProbeIndex = TraceTexelData.x & 0xFFFFF;
TraceTexelCoord.x = (TraceTexelData.x >> 20) & 0x1F;
TraceTexelCoord.y = (TraceTexelData.x >> 25) & 0x1F;
TraceHitDistance = asfloat(TraceTexelData.y);
}
void TraceMeshSDFs(
uint2 ScreenProbeAtlasCoord,
uint2 TraceTexelCoord,
uint ScreenProbeIndex,
float TraceHitDistance)
{
uint2 ScreenProbeScreenPosition = GetScreenProbeScreenPosition(ScreenProbeIndex);
uint2 ScreenTileCoord = GetScreenTileCoord(ScreenProbeScreenPosition);
uint ProbeTracingResolution = 0 ? ScreenProbeLightSampleResolutionXY : ScreenProbeTracingOctahedronResolution;
{
float2 ScreenUV = GetScreenUVFromScreenProbePosition(ScreenProbeScreenPosition);
float SceneDepth = GetScreenProbeDepth(ScreenProbeAtlasCoord);
uint2 TraceBufferCoord = GetTraceBufferCoord(ScreenProbeAtlasCoord, TraceTexelCoord);
float3 TranslatedWorldPosition = GetTranslatedWorldPositionFromScreenUV(ScreenUV, SceneDepth);
float3 WorldPosition = TranslatedWorldPosition - LWCToFloat( GetPrimaryView() .PreViewTranslation) ;
bool bHit = false;
bool bMoving = false;
{
float3 RayWorldDirection = 0;
float TraceDistance = MaxTraceDistance;
float ConeHalfAngle = 0;
GetScreenProbeTexelRay(
TraceBufferCoord,
TraceTexelCoord,
ScreenTileCoord,
TranslatedWorldPosition,
RayWorldDirection,
TraceDistance,
ConeHalfAngle);
float3 TranslatedSamplePosition = TranslatedWorldPosition + SurfaceBias * RayWorldDirection;
TranslatedSamplePosition += SurfaceBias * GetScreenProbeNormalForBiasing(ScreenProbeAtlasCoord, RayWorldDirection);
float3 SamplePosition = TranslatedSamplePosition - LWCToFloat( GetPrimaryView() .PreViewTranslation) ;
FConeTraceInput TraceInput;
TraceInput.Setup(SamplePosition, TranslatedSamplePosition, RayWorldDirection, ConeHalfAngle, MinSampleRadius, max(MinTraceDistance, TraceHitDistance - SurfaceBias * 2), MaxTraceDistance, StepFactor);
TraceInput.VoxelTraceStartDistance = min(MaxMeshSDFTraceDistance, TraceDistance);
TraceInput.bDitheredTransparency = true;
TraceInput.DitherScreenCoord = ScreenTileCoord * ProbeTracingResolution + TraceTexelCoord;
uint CardGridCellIndex = ComputeCardGridCellIndex(ScreenProbeScreenPosition - GetPrimaryView() .ViewRectMinAndSize.xy, SceneDepth);
TraceInput.NumMeshSDFs = NumGridCulledMeshSDFObjects[CardGridCellIndex];
TraceInput.MeshSDFStartOffset = GridCulledMeshSDFObjectStartOffsetArray[CardGridCellIndex];
TraceInput.CardInterpolateInfluenceRadius = CardInterpolateInfluenceRadius;
TraceInput.bCalculateHitVelocity = true;
FConeTraceResult TraceResult;
ConeTraceLumenSceneCards(TraceInput, TraceResult);
TraceInput.NumHeightfields = NumGridCulledHeightfieldObjects[CardGridCellIndex];
TraceInput.HeightfieldStartOffset = GridCulledHeightfieldObjectStartOffsetArray[CardGridCellIndex];
ConeTraceLumenSceneHeightfields(TraceInput, TraceResult);
float3 Lighting = TraceResult.Lighting;
float Transparency = TraceResult.Transparency;
float OpaqueHitDistance = TraceResult.OpaqueHitDistance;
{
float3 HitWorldPosition = SamplePosition + RayWorldDirection * OpaqueHitDistance;
bMoving = IsTraceMoving(WorldPosition, SceneDepth, ScreenProbeAtlasCoord, HitWorldPosition, TraceResult.WorldVelocity);
}
float DistanceFromViewpoint = length( LWCToFloat( GetPrimaryView() .WorldCameraOrigin) - WorldPosition);
float DistanceFade = saturate(6 * DistanceFromViewpoint / CardTraceEndDistanceFromCamera - 5);
Transparency = lerp(Transparency, 1, DistanceFade);
if (Transparency < InterleavedGradientNoise(TraceBufferCoord + 0.5f, 0))
{
bHit = true;
}
if (bHit)
{
Lighting *= 1 - DistanceFade;
Lighting += GetSkylightLeaking(RayWorldDirection, OpaqueHitDistance);
Lighting *= View_PreExposure;
RWTraceRadiance[TraceBufferCoord] = Lighting;
}
TraceHitDistance = OpaqueHitDistance + length(WorldPosition - SamplePosition);
}
WriteTraceHit(TraceBufferCoord, min(TraceHitDistance, MaxTraceDistance), bHit, bMoving);
}
}
void ConeTraceLumenSceneCards(
FConeTraceInput TraceInput,
inout FConeTraceResult OutResult)
{
OutResult = (FConeTraceResult)0;
OutResult.Transparency = 1;
OutResult.OpaqueHitDistance = TraceInput.MaxTraceDistance;
if (TraceInput.VoxelTraceStartDistance > TraceInput.MinTraceDistance)
{
FConeTraceInput CardTraceInput = TraceInput;
CardTraceInput.MaxTraceDistance = TraceInput.VoxelTraceStartDistance;
ConeTraceMeshSDFsAndInterpolateFromCards(CardTraceInput, OutResult);
}
}
void ConeTraceMeshSDFsAndInterpolateFromCards(
FConeTraceInput TraceInput,
inout FConeTraceResult OutResult)
{
FTraceMeshSDFResult TraceMeshSDFResult;
TraceMeshSDFResult.HitDistance = TraceInput.MaxTraceDistance;
TraceMeshSDFResult.HitObject = 0;
for (uint GridCulledMeshSDFIndex = 0; GridCulledMeshSDFIndex < TraceInput.NumMeshSDFs; GridCulledMeshSDFIndex++)
{
uint ObjectIndex = GridCulledMeshSDFObjectIndicesArray[TraceInput.MeshSDFStartOffset + GridCulledMeshSDFIndex];
RayTraceSingleMeshSDF(
TraceInput.ConeOrigin,
TraceInput.ConeDirection,
TraceInput.TanConeAngle,
TraceInput.MinTraceDistance,
TraceInput.MaxTraceDistance,
ObjectIndex,
TraceInput.bExpandSurfaceUsingRayTimeInsteadOfMaxDistance,
TraceInput.InitialMaxDistance,
TraceInput.bDitheredTransparency,
TraceInput.DitherScreenCoord,
TraceMeshSDFResult);
}
if (TraceMeshSDFResult.HitDistance < TraceInput.MaxTraceDistance)
{
FTraceMeshSDFDerivedData TraceSDFData = CalculateMeshSDFDerivedData(
TraceInput.ConeOrigin,
TraceInput.ConeDirection,
TraceInput.MaxTraceDistance,
TraceInput.bCalculateHitVelocity,
TraceMeshSDFResult);
float3 InterpolatePosition = TraceInput.ConeOrigin + TraceInput.ConeDirection * TraceMeshSDFResult.HitDistance;
float InterpolateRadius = TraceMeshSDFResult.HitDistance * TraceInput.TanConeAngle;
uint AtlasId = 0 ;
OutResult.Lighting = SampleLumenMeshCards(
TraceInput.DitherScreenCoord,
TraceSDFData.MeshCardsIndex,
InterpolatePosition,
TraceSDFData.HitNormal,
InterpolateRadius,
TraceSDFData.SurfaceCacheBias,
TraceInput.bHiResSurface,
AtlasId).Lighting;
OutResult.Transparency = 0;
OutResult.WorldVelocity = TraceSDFData.WorldVelocity;
}
OutResult.OpaqueHitDistance = TraceMeshSDFResult.HitDistance;
}
void RayTraceSingleMeshSDF(
float3 WorldRayStart,
float3 WorldRayDirection,
float TanConeHalfAngle,
float MinTraceDistance,
float MaxTraceDistance,
uint ObjectIndex,
// The SDF surface is expanded to reduce leaking through thin surfaces, especially foliage meshes with bGenerateDistanceFieldAsIfTwoSided
// Expanding as RayTime increases errors on the side of over-occlusion, especially at grazing angles, which can be desirable for diffuse GI.
// Expanding as MaxDistance increases has less incorrect self-intersection which is desirable for reflections rays.
bool bExpandSurfaceUsingRayTimeInsteadOfMaxDistance,
float InitialMaxDistance,
bool bDitheredTransparency,
float2 DitherScreenCoord,
inout FTraceMeshSDFResult TraceResult)
{
FDFObjectData DFObjectData = LoadDFObjectData(ObjectIndex);
float4x4 WorldToVolume = LWCHackToFloat(DFObjectData.WorldToVolume);
if (!bDitheredTransparency || !DFObjectData.bMostlyTwoSided)
{
// Trace up to the current hit point
MaxTraceDistance = min(MaxTraceDistance, TraceResult.HitDistance + DFObjectData.VolumeSurfaceBias);
}
float3 WorldRayEnd = WorldRayStart + WorldRayDirection * MaxTraceDistance;
float3 VolumeRayStart = mul(float4(WorldRayStart, 1), WorldToVolume).xyz;
float3 VolumeRayEnd = mul(float4(WorldRayEnd, 1), WorldToVolume).xyz;
float3 VolumeRayDirection = VolumeRayEnd - VolumeRayStart;
float VolumeMaxTraceDistance = length(VolumeRayDirection);
float VolumeMinTraceDistance = VolumeMaxTraceDistance * (MinTraceDistance / MaxTraceDistance);
VolumeRayDirection /= VolumeMaxTraceDistance;
float2 VolumeSpaceIntersectionTimes = LineBoxIntersect(VolumeRayStart, VolumeRayEnd, -DFObjectData.VolumePositionExtent, DFObjectData.VolumePositionExtent);
VolumeSpaceIntersectionTimes *= VolumeMaxTraceDistance;
VolumeSpaceIntersectionTimes.x = max(VolumeSpaceIntersectionTimes.x, VolumeMinTraceDistance);
BRANCH
if (VolumeSpaceIntersectionTimes.x < VolumeSpaceIntersectionTimes.y)
{
uint MaxMipIndex = LoadDFAssetData(DFObjectData.AssetIndex, 0).NumMips - 1;
// Start tracing at the highest resolution mip
uint ReversedMipIndex = MaxMipIndex;
FDFAssetData DFAssetMipData = LoadDFAssetData(DFObjectData.AssetIndex, ReversedMipIndex);
#if !SDF_TRACING_TRAVERSE_MIPS
ReversedMipIndex = MaxMipIndex;
DFAssetMipData = LoadDFAssetData(DFObjectData.AssetIndex, ReversedMipIndex);
#endif
float Coverage = DFObjectData.bMostlyTwoSided && bDitheredTransparency ? 0.0f : 1.0f;
float ExpandSurfaceScale = lerp(MeshSDFNotCoveredExpandSurfaceScale, 1.0f, Coverage);
float SampleRayTime = VolumeSpaceIntersectionTimes.x;
uint MaxSteps = 64;
float MinStepSize = 1.0f / (16.0f * MaxSteps);
uint StepIndex = 0;
bool bHit = false;
float MaxDistance = InitialMaxDistance;
LOOP
for (; StepIndex < MaxSteps; StepIndex++)
{
float3 SampleVolumePosition = VolumeRayStart + VolumeRayDirection * SampleRayTime;
float DistanceField = SampleSparseMeshSignedDistanceField(SampleVolumePosition, DFAssetMipData);
MaxDistance = max(DistanceField, MaxDistance);
float ExpandSurfaceTime = bExpandSurfaceUsingRayTimeInsteadOfMaxDistance ? SampleRayTime : MaxDistance;
// Expand the surface to find thin features, but only away from the start of the trace where it won't introduce incorrect self-occlusion
// This still causes incorrect self-occlusion at grazing angles
float ExpandSurfaceDistance = DFObjectData.VolumeSurfaceBias;
const float ExpandSurfaceFalloff = 2.0f * ExpandSurfaceDistance;
const float ExpandSurfaceAmount = ExpandSurfaceDistance * saturate(ExpandSurfaceTime / ExpandSurfaceFalloff) * ExpandSurfaceScale;
float StepNoise = InterleavedGradientNoise(DitherScreenCoord.xy, View.StateFrameIndexMod8 * MaxSteps + StepIndex);
#if SDF_TRACING_TRAVERSE_MIPS
float MaxEncodedDistance = DFAssetMipData.DistanceFieldToVolumeScaleBias.x + DFAssetMipData.DistanceFieldToVolumeScaleBias.y;
// We reached the maximum distance of this mip's narrow band, use a lower resolution mip for next iteration
if (abs(DistanceField) > MaxEncodedDistance && ReversedMipIndex > 0)
{
ReversedMipIndex--;
DFAssetMipData = LoadDFAssetData(DFObjectData.AssetIndex, ReversedMipIndex);
}
// We are close to the surface, step back to safety and use a higher resolution mip for next iteration
else if (abs(DistanceField) < .25f * MaxEncodedDistance && ReversedMipIndex < MaxMipIndex)
{
DistanceField -= 6.0f * DFObjectData.VolumeSurfaceBias;
ReversedMipIndex++;
DFAssetMipData = LoadDFAssetData(DFObjectData.AssetIndex, ReversedMipIndex);
}
else
#endif
if (DistanceField < ExpandSurfaceAmount
&& ReversedMipIndex == MaxMipIndex
&& (!bDitheredTransparency || StepNoise * (1 - Coverage) <= MeshSDFDitheredTransparencyStepThreshold))
{
// One more step to the surface
// Pull back by ExpandSurfaceAmount to improve the gradient computed off of the hit point
SampleRayTime = clamp(SampleRayTime + DistanceField - ExpandSurfaceAmount, VolumeSpaceIntersectionTimes.x, VolumeSpaceIntersectionTimes.y);
bHit = true;
break;
}
float LocalMinStepSize = MinStepSize * lerp(MeshSDFNotCoveredMinStepScale, 1.0f, Coverage);
float StepDistance = max(DistanceField, LocalMinStepSize);
SampleRayTime += StepDistance;
if (SampleRayTime > VolumeSpaceIntersectionTimes.y + ExpandSurfaceAmount)
{
break;
}
}
if (StepIndex == MaxSteps)
{
bHit = true;
}
if (bHit)
{
float NewHitDistance = length(VolumeRayDirection * SampleRayTime * DFObjectData.VolumeToWorldScale);
if (NewHitDistance < TraceResult.HitDistance)
{
TraceResult.HitObject = ObjectIndex;
TraceResult.HitDistance = NewHitDistance;
}
}
}
}
/**
* Sample surface cache
*/
FSurfaceCacheSample SampleLumenMeshCards(
uint2 ScreenCoord,
uint MeshCardsIndex,
float3 WorldSpacePosition,
float3 WorldSpaceNormal,
float SampleRadius, // Cone footprint for mip map selection and feedback
float SurfaceCacheBias, // Card bounds and depth test bias in mesh space. Useful when traced geometry doesn't match the original mesh due to SDFs or geometry LOD
bool bHiResSurface, // Whether to allow to sample high res surface pages or use only the lowest quality ones (always resident)
uint AtlasId)
{
FSurfaceCacheSample SurfaceCacheSample = InitSurfaceCacheSample();
#if ENABLE_VISUALIZE_MODE == 1
if (VisualizeMode == VISUALIZE_MODE_SURFACE_CACHE || VisualizeMode == VISUALIZE_MODE_OPACITY)
{
SurfaceCacheSample.Lighting = (MeshCardsIndex < LumenCardScene.NumMeshCards ? float3(1, 0, 1) : float3(1, 1, 0)) * View.OneOverPreExposure;
}
#endif
if (MeshCardsIndex < LumenCardScene.NumMeshCards)
{
FLumenMeshCardsData MeshCardsData = GetLumenMeshCardsData(MeshCardsIndex);
float3 MeshCardsSpacePosition = mul(WorldSpacePosition - MeshCardsData.WorldOrigin, MeshCardsData.WorldToLocalRotation);
float3 MeshCardsSpaceNormal = mul(WorldSpaceNormal, MeshCardsData.WorldToLocalRotation);
uint CardMask = 0;
float3 AxisWeights = MeshCardsSpaceNormal * MeshCardsSpaceNormal;
// Pick cards by angle
if (AxisWeights.x > 0.0f)
{
CardMask |= MeshCardsData.CardLookup[MeshCardsSpaceNormal.x < 0.0f ? 0 : 1];
}
if (AxisWeights.y > 0.0f)
{
CardMask |= MeshCardsData.CardLookup[MeshCardsSpaceNormal.y < 0.0f ? 2 : 3];
}
if (AxisWeights.z > 0.0f)
{
CardMask |= MeshCardsData.CardLookup[MeshCardsSpaceNormal.z < 0.0f ? 4 : 5];
}
// Cull cards by AABB
{
uint CulledCardMask = 0;
while (CardMask != 0)
{
const uint NextBitIndex = firstbitlow(CardMask);
const uint NextBitMask = 1u << NextBitIndex;
CardMask ^= NextBitMask;
uint CardIndex = MeshCardsData.CardOffset + NextBitIndex;
FLumenCardData LumenCardData = GetLumenCardData(CardIndex);
if (all(abs(MeshCardsSpacePosition - LumenCardData.MeshCardsOrigin) <= LumenCardData.MeshCardsExtent + 0.5f * SurfaceCacheBias))
{
CulledCardMask |= NextBitMask;
}
}
CardMask = CulledCardMask;
}
if (MeshCardsData.bHeightfield)
{
CardMask = (1 << LUMEN_HEIGHTFIELD_LOCAL_CARD_INDEX);
}
FCardSampleAccumulator CardSampleAccumulator;
InitCardSampleAccumulator(CardSampleAccumulator);
// Sample cards
while (CardMask != 0)
{
const uint NextBitIndex = firstbitlow(CardMask);
CardMask ^= 1u << NextBitIndex;
uint CardIndex = MeshCardsData.CardOffset + NextBitIndex;
FLumenCardData LumenCardData = GetLumenCardData(CardIndex);
if (LumenCardData.bVisible)
{
SampleLumenCard(
MeshCardsSpacePosition,
MeshCardsSpaceNormal,
SampleRadius,
SurfaceCacheBias,
CardIndex,
AxisWeights,
bHiResSurface,
MeshCardsData.bHeightfield,
AtlasId,
CardSampleAccumulator);
}
}
if (CardSampleAccumulator.SampleWeightSum > 0.0f)
{
SurfaceCacheSample.LightingSum = CardSampleAccumulator.LightingSum;
SurfaceCacheSample.OpacitySum = CardSampleAccumulator.OpacitySum;
SurfaceCacheSample.SampleWeightSum = CardSampleAccumulator.SampleWeightSum;
SurfaceCacheSample.Lighting = CardSampleAccumulator.LightingSum / CardSampleAccumulator.SampleWeightSum;
SurfaceCacheSample.Opacity = CardSampleAccumulator.OpacitySum / CardSampleAccumulator.SampleWeightSum;
SurfaceCacheSample.bValid = true;
}
#if SURFACE_CACHE_FEEDBACK
{
// Write every n-th element
if (all((ScreenCoord & SurfaceCacheFeedbackBufferTileWrapMask) == SurfaceCacheFeedbackBufferTileJitter)
&& SurfaceCacheFeedbackBufferSize > 0
&& CardSampleAccumulator.SampleWeightSum > 0.1f)
{
#if SURFACE_CACHE_HIGH_RES_PAGES
{
uint WriteOffset = 0;
InterlockedAdd(RWSurfaceCacheFeedbackBufferAllocator[0], 1, WriteOffset);
if (WriteOffset < SurfaceCacheFeedbackBufferSize)
{
RWSurfaceCacheFeedbackBuffer[WriteOffset] = CardSampleAccumulator.CardSample.PackedFeedback;
}
RWCardPageHighResLastUsedBuffer[CardSampleAccumulator.CardSample.CardPageIndex] = SurfaceCacheUpdateFrameIndex;
}
#else
{
RWCardPageLastUsedBuffer[CardSampleAccumulator.CardSample.CardPageIndex] = SurfaceCacheUpdateFrameIndex;
}
#endif
}
}
#endif
// Debug visualization
#if ENABLE_VISUALIZE_MODE == 1
{
if (VisualizeMode == VISUALIZE_MODE_GEOMETRY_NORMALS)
{
SurfaceCacheSample.Lighting = (WorldSpaceNormal * 0.5f + 0.5f) * View.OneOverPreExposure;
}
}
#endif
}
return SurfaceCacheSample;
}
FLumenCardSample ComputeSurfaceCacheSample(FLumenCardData Card, uint CardIndex, float2 LocalSamplePosition, float SampleRadius, bool bHiResSurface)
{
// CardUV in [0;1)
float2 CardUV = min(SamplePositonToCardUV(Card, LocalSamplePosition), 0.999999f);
uint2 SizeInPages = Card.SizeInPages;
uint PageTableOffset = Card.PageTableOffset;
if (bHiResSurface)
{
SizeInPages = Card.HiResSizeInPages;
PageTableOffset = Card.HiResPageTableOffset;
}
uint2 PageCoord = CardUV * SizeInPages;
uint LinearPageCoord = PageCoord.x + PageCoord.y * SizeInPages.x;
const uint PageTableIndex = PageTableOffset + LinearPageCoord;
const uint2 PageTableValue = LumenCardScene.PageTableBuffer.Load2(8 * PageTableIndex);
uint2 AtlasBias;
AtlasBias.x = ((PageTableValue.x >> 0) & 0xFFF) * MIN_CARD_RESOLUTION;
AtlasBias.y = ((PageTableValue.x >> 12) & 0xFFF) * MIN_CARD_RESOLUTION;
uint2 ResLevelXY;
ResLevelXY.x = (PageTableValue.x >> 24) & 0xF;
ResLevelXY.y = (PageTableValue.x >> 28) & 0xF;
// Mapped page index (sampled page may be pointing to another mip map level)
const uint CardPageIndex = PageTableValue.y;
// Recompute new SizeInPages and PageCoord, as sampled page may be pointing to an another mip map level
SizeInPages = ResLevelXYToSizeInPages(ResLevelXY);
PageCoord = CardUV * SizeInPages;
uint2 AtlasScale = select(ResLevelXY > SUB_ALLOCATION_RES_LEVEL, PHYSICAL_PAGE_SIZE, (1u << ResLevelXY));
float2 PageUV = frac(CardUV * SizeInPages);
// Page edges (which aren't card edges) need to be remapped from [0; PageSize] to [0.5; PageSize - 0.5]
// for correct bilinear filtering between pages and not reading texels outside of that page
float2 MinUVBorder = select(PageCoord.xy == 0, 0.0f, 0.5f);
float2 MaxUVBorder = select(PageCoord.xy + 1 == SizeInPages.xy, 0.0f, 0.5f);
float2 CoordInPage = (PageUV * (AtlasScale - MinUVBorder - MaxUVBorder)) + MinUVBorder;
// Card edges need to be clamped to [0.5; CardResolution - 1 - 0.5] so that bilinear filtering doesn't read texels from other cards
CoordInPage = clamp(CoordInPage, 0.5f, AtlasScale - 1.0f - 0.5f);
float2 PhysicalAtlasUV = (CoordInPage + AtlasBias) * LumenCardScene.InvPhysicalAtlasSize;
// Indirect lighting can be sampled from a downsampled atlas
float ILFactor = LumenCardScene.IndirectLightingAtlasDownsampleFactor;
float2 IndirectLightingPhysicalAtlasUV = (PageUV * (AtlasScale / ILFactor - 1.0f) + AtlasBias / ILFactor + 0.5f) * ILFactor * LumenCardScene.InvPhysicalAtlasSize;
// Compute packed feedback buffer value
uint2 PackedFeedback = 0;
#if SURFACE_CACHE_FEEDBACK && SURFACE_CACHE_HIGH_RES_PAGES
{
// Compute optimal res level, based on the cone width (SampleRadius)
float SampleResolution = max(Card.LocalExtent.x, Card.LocalExtent.y) / max(SampleRadius, 1.0f);
uint DesiredResLevel = clamp(log2(SampleResolution) + SurfaceCacheFeedbackResLevelBias, MIN_RES_LEVEL, MAX_RES_LEVEL);
uint2 LevelSizeInPages = GetSizeInPages(Card, DesiredResLevel);
uint2 LocalPageCoord = CardUV * LevelSizeInPages;
PackedFeedback.x = CardIndex | (DesiredResLevel << 24);
PackedFeedback.y = LocalPageCoord.x + (LocalPageCoord.y << 8);
}
#endif
float2 FracUV = frac(PhysicalAtlasUV * LumenCardScene.PhysicalAtlasSize + 0.5f + 1.0f / 512.0f);
float4 TexelBilinearWeights;
TexelBilinearWeights.x = (1.0 - FracUV.x) * (FracUV.y);
TexelBilinearWeights.y = (FracUV.x) * (FracUV.y);
TexelBilinearWeights.z = (FracUV.x) * (1 - FracUV.y);
TexelBilinearWeights.w = (1 - FracUV.x) * (1 - FracUV.y);
FLumenCardSample CardSample;
CardSample.CardIndex = CardIndex;
CardSample.CardPageIndex = CardPageIndex;
CardSample.PhysicalAtlasUV = PhysicalAtlasUV;
CardSample.TexelBilinearWeights = TexelBilinearWeights;
CardSample.IndirectLightingPhysicalAtlasUV = IndirectLightingPhysicalAtlasUV;
CardSample.bValid = ResLevelXY.x > 0;
CardSample.PackedFeedback = PackedFeedback;
return CardSample;
}
void SampleLumenCard(
float3 MeshCardsSpacePosition,
float3 MeshCardsSpaceNormal,
float SampleRadius,
float SurfaceCacheBias,
uint CardIndex,
float3 AxisWeights,
bool bHiResSurface,
bool bHeightfield,
uint AtlasId,
inout FCardSampleAccumulator CardSampleAccumulator)
{
if (CardIndex < LumenCardScene.NumCards)
{
FLumenCardData LumenCardData = GetLumenCardData(CardIndex);
if (LumenCardData.bVisible)
{
float3 CardSpacePosition = mul(MeshCardsSpacePosition - LumenCardData.MeshCardsOrigin, LumenCardData.MeshCardsToLocalRotation);
if (all(abs(CardSpacePosition) <= LumenCardData.LocalExtent + 0.5f * SurfaceCacheBias))
{
CardSpacePosition.xy = clamp(CardSpacePosition.xy, -LumenCardData.LocalExtent.xy, LumenCardData.LocalExtent.xy);
FLumenCardSample CardSample = ComputeSurfaceCacheSample(LumenCardData, CardIndex, CardSpacePosition.xy, SampleRadius, bHiResSurface);
if (CardSample.bValid)
{
// Card projection angle
float NormalWeight = 1.0f;
if (!bHeightfield)
{
if (LumenCardData.AxisAlignedDirection < 2)
{
NormalWeight = AxisWeights.x;
}
else if (LumenCardData.AxisAlignedDirection < 4)
{
NormalWeight = AxisWeights.y;
}
else
{
NormalWeight = AxisWeights.z;
}
}
if (NormalWeight > 0.0f)
{
float4 TexelDepths = DepthAtlas.Gather(GlobalPointClampedSampler, CardSample.PhysicalAtlasUV, 0.0f);
float NormalizedHitDistance = -(CardSpacePosition.z / LumenCardData.LocalExtent.z) * 0.5f + 0.5f;
float BiasTreshold = SurfaceCacheBias / LumenCardData.LocalExtent.z;
float BiasFalloff = 0.25f * BiasTreshold;
float4 TexelVisibility = 0.0f;
for (uint TexelIndex = 0; TexelIndex < 4; ++TexelIndex)
{
// Skip invalid texels
if (IsSurfaceCacheDepthValid(TexelDepths[TexelIndex]))
{
// No need to depth test heightfields
if (bHeightfield)
{
TexelVisibility[TexelIndex] = 1.0f;
}
else
{
TexelVisibility[TexelIndex] = 1.0f - saturate((abs(NormalizedHitDistance - TexelDepths[TexelIndex]) - BiasTreshold) / BiasFalloff);
}
}
}
float4 TexelWeights = CardSample.TexelBilinearWeights * TexelVisibility;
float CardSampleWeight = NormalWeight * dot(TexelWeights, 1.0f);
if (CardSampleWeight > 0.0f)
{
// Normalize weights
float TexelWeightSum = dot(TexelWeights, 1.0f);
TexelWeights /= TexelWeightSum;
float Opacity = SampleSurfaceCacheAtlas(OpacityAtlas, CardSample.PhysicalAtlasUV, TexelWeights).x;
float3 Lighting = 0.0f;
if (AtlasId == FINAL_LIGHTING_ATLAS_ID)
{
Lighting = SampleSurfaceCacheAtlas(FinalLightingAtlas, CardSample.PhysicalAtlasUV, TexelWeights);
}
else if (AtlasId == IRRADIANCE_ATLAS_ID)
{
float3 DirectLighting = SampleSurfaceCacheAtlas(DirectLightingAtlas, CardSample.PhysicalAtlasUV, TexelWeights);
float3 IndirectLighting = SampleSurfaceCacheAtlas(IndirectLightingAtlas, CardSample.IndirectLightingPhysicalAtlasUV, TexelWeights);
Lighting = DirectLighting + IndirectLighting;
}
else // if (AtlasId == INDIRECT_IRRADIANCE_ATLAS_ID)
{
Lighting = SampleSurfaceCacheAtlas(IndirectLightingAtlas, CardSample.PhysicalAtlasUV, TexelWeights);
}
// Debug visualization
#if ENABLE_VISUALIZE_MODE == 1
{
if (VisualizeMode == VISUALIZE_MODE_DIRECT_LIGHTING)
{
Lighting = SampleSurfaceCacheAtlas(DirectLightingAtlas, CardSample.PhysicalAtlasUV, TexelWeights);
}
else if (VisualizeMode == VISUALIZE_MODE_INDIRECT_LIGHTING)
{
Lighting = SampleSurfaceCacheAtlas(IndirectLightingAtlas, CardSample.IndirectLightingPhysicalAtlasUV, TexelWeights);
}
else if (VisualizeMode == VISUALIZE_MODE_ALBEDO)
{
Lighting = DecodeSurfaceCacheAlbedo(SampleSurfaceCacheAtlas(AlbedoAtlas, CardSample.PhysicalAtlasUV, TexelWeights)) * View.OneOverPreExposure;
}
else if (VisualizeMode == VISUALIZE_MODE_NORMALS)
{
FLumenCardData Card = GetLumenCardData(CardSample.CardIndex);
float3 WorldSpaceNormal = DecodeSurfaceCacheNormal(Card, SampleSurfaceCacheAtlas(NormalAtlas, CardSample.PhysicalAtlasUV, TexelWeights).xy);
Lighting = (WorldSpaceNormal * 0.5f + 0.5f) * View.OneOverPreExposure;
}
else if (VisualizeMode == VISUALIZE_MODE_EMISSIVE)
{
Lighting = SampleSurfaceCacheAtlas(EmissiveAtlas, CardSample.PhysicalAtlasUV, TexelWeights);
}
else if (VisualizeMode == VISUALIZE_MODE_OPACITY)
{
Lighting = Opacity * View.OneOverPreExposure;
Opacity = 1.0f;
}
else if (VisualizeMode == VISUALIZE_MODE_CARD_WEIGHTS)
{
float3 RandomColor;
RandomColor.x = (CardSample.CardIndex % 4) / 3.0f;
RandomColor.y = ((CardSample.CardIndex / 4) % 4) / 3.0f;
RandomColor.z = saturate(1.0f - RandomColor.x - RandomColor.y);
// UV Grid overlay
float2 UVGrid = frac(CardSample.PhysicalAtlasUV * LumenCardScene.PhysicalAtlasSize / 8.0f);
UVGrid = smoothstep(abs(UVGrid - 0.5f), 0.0f, 0.01f);
RandomColor *= lerp(0.25f, 1.0f, saturate(UVGrid.x * UVGrid.y));
Lighting = RandomColor * View.OneOverPreExposure;
}
else if (VisualizeMode == VISUALIZE_MODE_DIRECT_LIGHTING_UPDATES || VisualizeMode == VISUALIZE_MODE_INDIRECT_LIGHTING_UPDATES || VisualizeMode == VISUALIZE_MODE_LAST_USED_PAGE || VisualizeMode == VISUALIZE_MODE_LAST_USED_PAGE_HIGH_RES)
{
FLumenCardPageData LumenCardPage = GetLumenCardPageData(CardSample.CardPageIndex);
uint LastUpdateFrameIndex = 0;
float VisScale = 8.0f;
if (VisualizeMode == VISUALIZE_MODE_DIRECT_LIGHTING_UPDATES)
{
LastUpdateFrameIndex = LumenCardPage.LastDirectLightingUpdateFrameIndex;
}
else if (VisualizeMode == VISUALIZE_MODE_INDIRECT_LIGHTING_UPDATES)
{
LastUpdateFrameIndex = LumenCardPage.LastIndirectLightingUpdateFrameIndex;
VisScale = 32.0f;
}
else if (VisualizeMode == VISUALIZE_MODE_LAST_USED_PAGE)
{
#if SURFACE_CACHE_FEEDBACK
LastUpdateFrameIndex = RWCardPageLastUsedBuffer[CardSample.CardPageIndex];
#endif
}
else if (VisualizeMode == VISUALIZE_MODE_LAST_USED_PAGE_HIGH_RES)
{
#if SURFACE_CACHE_FEEDBACK
LastUpdateFrameIndex = RWCardPageHighResLastUsedBuffer[CardSample.CardPageIndex];
#endif
}
uint FramesSinceLastUpdated = SurfaceCacheUpdateFrameIndex - LastUpdateFrameIndex;
float3 VisColor = lerp(float3(1, 0, 0), float3(0, 0, 1), saturate(FramesSinceLastUpdated / VisScale));
if (FramesSinceLastUpdated < 1)
{
VisColor = float3(1, 1, 1);
}
Lighting = VisColor * View.OneOverPreExposure;
}
}
#endif
CardSampleAccumulator.LightingSum += Lighting * CardSampleWeight;
CardSampleAccumulator.OpacitySum += Opacity * CardSampleWeight;
CardSampleAccumulator.SampleWeightSum += CardSampleWeight;
// Pick single sample based on the max weight
if (CardSampleWeight > CardSampleAccumulator.MaxSampleWeight)
{
CardSampleAccumulator.CardSample = CardSample;
CardSampleAccumulator.MaxSampleWeight = CardSampleWeight;
}
}
}
}
}
}
}
}