// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================================= PathTracingSpatialTemporalDenoising.usf: Spatial temporal denoising for path tracing ===============================================================================================*/ #pragma once #include "/Engine/Public/Platform.ush" #include "../ScreenPass.ush" #include "../../Private/Common.ush" #include "../TextureSampling.ush" #include "../ColorDifference.ush" #ifndef THREAD_SIZE #define THREAD_SIZE 8 #define THREAD_SIZE_X THREAD_SIZE #define THREAD_SIZE_Y THREAD_SIZE #endif #ifndef TEMPORAL_REPROJECTION_ALIGN #define TEMPORAL_REPROJECTION_ALIGN 0 #endif #ifndef TEMPORAL_REPROJECTION_BLUR #define TEMPORAL_REPROJECTION_BLUR 0 #define DIRECT_COPY 0 #endif #ifndef TEMPORAL_REPROJECTION_MERGE #define TEMPORAL_REPROJECTION_MERGE 0 #endif #ifndef TEMPORAL_REPROJECTION_RESOLVE #define TEMPORAL_REPROJECTION_RESOLVE 0 #endif #ifndef TEMPORAL_FEATURE_FUSION #define TEMPORAL_FEATURE_FUSION 0 #endif // Variance Type #define RADIANCE_MULTI_CHANNEL 0 #define RADIANCE_ALBEDO_NORMAL_SINGLECHANNEL 1 #ifndef TEMPORAL_PREPASS #define TEMPORAL_PREPASS 0 #define VARIANCE_TYPE RADIANCE_MULTI_CHANNEL #endif #define PREPASS_PHASE_INIT 0 #define PREPASS_PHASE_UPDATE 1 #ifndef RANKED_LUMINANCE_VARIANCE #define RANKED_LUMINANCE_VARIANCE 0 #endif #ifndef SPATIAL_DENOISING #define SPATIAL_DENOISING 0 #endif #ifndef PREPASS_GENERATE_TEXTURE #define PREPASS_GENERATE_TEXTURE 0 #endif #ifndef VISUALIZE_MOTIONVECTOR #define VISUALIZE_MOTIONVECTOR 0 #endif #ifndef MOTION_VECTOR_SUBTRACT #define MOTION_VECTOR_SUBTRACT 0 #endif #ifndef TEMPORAL_HIGHFREQ_REJECT #define TEMPORAL_HIGHFREQ_REJECT 0 #endif #ifndef VISUALIZE_WARPING #define VISUALIZE_WARPING 0 #endif #ifndef TOTAL_VARIATION #define TOTAL_VARIATION 0 #endif #ifndef PREPROCESS_BUFFER #define PREPROCESS_BUFFER 0 #endif // Define the distance metrics to use #define METRICS_LUMINANCE 0 #define METRICS_EUCLIDEAN 1 #define METRICS_PERCEPTION 2 #ifndef DISTANCE_METRICS #define DISTANCE_METRICS METRICS_LUMINANCE #endif // Gamma correction improves the reprojection of euclidean and luminance // based distance metrics #define APPLY_SIMPLIFIED_GAMMA (DISTANCE_METRICS == METRICS_EUCLIDEAN || \ DISTANCE_METRICS == METRICS_LUMINANCE) #ifndef K_NUM_OF_TEXTURES_PER_PASS #define K_NUM_OF_TEXTURES_PER_PASS 7 #endif #ifndef K_NUM_OF_SHIFTS_PER_SLICE #define K_NUM_OF_SHIFTS_PER_SLICE 4 #endif // the mip difference is 2 instead of one to make use of coarser mips #define MIP_DIFF_DELTA 2 // The motion vector estimation algorithm is inspired from // Hanika, J., Tessari, L., & Dachsbacher, C. (2021). // Fast Temporal Reprojection without Motion Vectors. Journal of Computer Graphics Techniques Vol, 10(3). // Distance type (L1, L2, logL1, logL1^C) #define DISTANCE_L1 0 #define DISTANCE_L2 1 #define DISTANCE_LOG_L1 2 #define DISTANCE_LOG_REGULARIZED_L1 3 #define DISTANCE_TYPE DISTANCE_LOG_REGULARIZED_L1 SCREEN_PASS_TEXTURE_VIEWPORT(TargetViewport) struct FPixelOffset { float2 xy; float Dist; float TV; }; struct FPixelMaterialLightingFingerprint { float4 Mean; float4 Var; }; void Max3Index(in float3 Value, out int MaxIndex) { MaxIndex = (Value.x > Value.y) ? 0 : 1; MaxIndex = Value[MaxIndex] > Value.z ? MaxIndex : 2; } void Max3(in float3 Value, out float MaxValue, out int MaxIndex) { MaxIndex = (Value.x > Value.y) ? 0 : 1; MaxIndex = Value[MaxIndex] > Value.z ? MaxIndex : 2; MaxValue = Value[MaxIndex]; } void Min3Index(in float3 Value, out int MinIndex) { MinIndex = (Value.x <= Value.y) ? 0 : 1; MinIndex = Value[MinIndex] <= Value.z ? MinIndex : 2; } void Min3(in float3 Value, out float MinValue, out int MinIndex) { MinIndex = (Value.x <= Value.y) ? 0 : 1; MinIndex = Value[MinIndex] <= Value.z ? MinIndex : 2; MinValue = Value[MinIndex]; } #if K_NUM_OF_TEXTURES_PER_PASS <= 7 && (TEMPORAL_REPROJECTION_ALIGN||TEMPORAL_REPROJECTION_MERGE) static int2 Shifts[25] = { {-2,-2},{-1,-2},{0,-2},{1,-2},{2,-2}, {-2,-1},{-1,-1},{0,-1},{1,-1},{2,-1}, {-2, 0},{-1, 0},{0, 0},{1, 0},{2, 0}, {-2, 1},{-1, 1},{0, 1},{1, 1},{2, 1}, {-2, 2},{-1, 2},{0, 2},{1, 2},{2, 2}, }; RWTexture2D RWDistanceTextures_0; RWTexture2D RWDistanceTextures_1; RWTexture2D RWDistanceTextures_2; RWTexture2D RWDistanceTextures_3; RWTexture2D RWDistanceTextures_4; RWTexture2D RWDistanceTextures_5; RWTexture2D RWDistanceTextures_6; Texture2D DistanceTextures_0; Texture2D DistanceTextures_1; Texture2D DistanceTextures_2; Texture2D DistanceTextures_3; Texture2D DistanceTextures_4; Texture2D DistanceTextures_5; Texture2D DistanceTextures_6; void SaveOffsets(int2 Position, int BasePixelShift, float4 Value) { if (BasePixelShift < 4) { RWDistanceTextures_0[Position] = Value; } else if (BasePixelShift < 8) { RWDistanceTextures_1[Position] = Value; } else if (BasePixelShift < 12) { RWDistanceTextures_2[Position] = Value; } else if (BasePixelShift < 16) { RWDistanceTextures_3[Position] = Value; } else if (BasePixelShift < 20) { RWDistanceTextures_4[Position] = Value; } else if (BasePixelShift < 24) { RWDistanceTextures_5[Position] = Value; } else if (BasePixelShift < 28) { RWDistanceTextures_6[Position] = Value; } } float GetOffsets(int2 Position, int2 Offset) { Offset = clamp(Offset, int2(-2, -2), int2(2, 2)); uint Index = Offset.x + Offset.y * 5 + 12; uint TextureIndexOffset = Index % 4; int2 ShiftedPosition = Position + Offset; //@TODO: range check float Ret = 0.0f; if (Index < 4) { Ret = DistanceTextures_0[ShiftedPosition][TextureIndexOffset]; } else if (Index < 8) { Ret = DistanceTextures_1[ShiftedPosition][TextureIndexOffset]; } else if (Index < 12) { Ret = DistanceTextures_2[ShiftedPosition][TextureIndexOffset]; } else if (Index < 16) { Ret = DistanceTextures_3[ShiftedPosition][TextureIndexOffset]; } else if (Index < 20) { Ret = DistanceTextures_4[ShiftedPosition][TextureIndexOffset]; } else if (Index < 24) { Ret = DistanceTextures_5[ShiftedPosition][TextureIndexOffset]; } else if (Index < 28) { Ret = DistanceTextures_6[ShiftedPosition][TextureIndexOffset]; } return Ret; } struct FQuadricFittingContext { int2 Offset; // integer shift of the minimal value float3x3 Values; // holding the surrounding distance to fit a bivariate polynomial. }; float Min3(float3 value) { return min(min(value.x, value.y), value.z); } float Min4(float4 value) { return min(min(value.x, value.y), min(value.z, value.w)); } struct FFindMinContext { int TextureOffset; int Index; float Value; }; void Min4(in float4 Value, out float MinValue, out int MinIndex) { int MinIndexXy = (Value.x <= Value.y) ? 0 : 1; int MinIndexZw = (Value.z <= Value.w) ? 2 : 3; MinIndex = (Value[MinIndexXy] <= Value[MinIndexZw]) ? MinIndexXy : MinIndexZw; MinValue = Value[MinIndex]; } void Max4(in float4 Value, out float MaxValue, out int MaxIndex) { int MaxIndexXy = (Value.x < Value.y) ? 1 : 0; int MaxIndexZw = (Value.z < Value.w) ? 3 : 2; MaxIndex = (Value[MaxIndexXy] < Value[MaxIndexZw]) ? MaxIndexZw : MaxIndexXy; MaxValue = Value[MaxIndex]; } void UpdateMin4Index(int TextureOffset, float4 Value, inout FFindMinContext Context) { int MinIndex; float MinValue; Min4(Value, MinValue, MinIndex); if (MinValue < Context.Value) { Context.TextureOffset = TextureOffset; Context.Index = MinIndex; Context.Value = MinValue; } } #define ESTIMATE_SUBPIXELOFFSET SUBPIXEL_OFFSET #if ESTIMATE_SUBPIXELOFFSET // Ref: Section 4 of the Appendix of "Burst photography for high dynamic range and low-light imaging on mobile cameras" // https://dl.acm.org/doi/10.1145/2980179.2980254, 2016 // solve a weighted least-squares problem with weight of // 1 2 1 // 2 4 2 // 1 2 1 static const float FA11[9] = { 1.0f, -2.0f, 1.0f, 2.0f, -4.0f, 2.0f, 1.0f, -2.0f, 1.0f}; static const float FA22[9] = { 1.0f / 4.0f, 2.0f / 4.0f, 1.0f / 4.0f, -2.0f / 4.0f, -4.0f / 4.0f, -2.0f / 4.0f, 1.0f / 4.0f, 2.0f / 4.0f, 1.0f / 4.0f }; static const float FA12[9] = { 1.0f / 4.0f, 0.0f, -1.0f / 4.0f, 0.0f, 0.0f, 0.0f, -1.0f / 4.0f, 0.0f, 1.0f / 4.0f }; // FA12 == FA21 static const float FB1[9] = { -1.0f / 8.0f, 0.0f, 1.0f / 8.0f, -2.0f / 8.0f, 0.0f, 2.0f / 8.0f, -1.0f / 8.0f, 0.0f, 1.0f / 8.0f }; static const float FB2[9] = { -1.0f / 8.0f, -2.0f / 8.0f, -1.0f / 8.0f, 0.0f, 0.0f, 0.0f, 1.0f / 8.0f, 2.0f / 8.0f, 1.0f / 8.0f }; //static const float FC[] = { -1.0f, 2.0f, -1.0f, // 2.0f, 12.0f, 2.0f, // -1.0f, 2.0f, -1.0f } / 16; float Dot9(float A[9], float B[9]) { return A[0] * B[0] + A[1] * B[1] + A[2] * B[2] + A[3] * B[3] + A[4] * B[4] + A[5] * B[5] + A[6] * B[6] + A[7] * B[7] + A[8] * B[8]; } // D holds the 3x3 image distance. Return the subpixel offset // in range [-1, 1] x [-1, 1] float2 EstimateSubpixelOffset(float3x3 D) { // Step 1. Construct the constant coefficient A, b, c // for the bivariate quadratic function. float DSub[9]; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) DSub[3 * (j + 1) + i + 1] = D[j + 1][i + 1]; // force A positive semi-definite (PSD) with max(0, Aij) and reset non-diag // to zero if det is negative. // note: A12 == A21 float4 A = { max(0.0f, Dot9(FA11, DSub)), Dot9(FA12, DSub), 0.0f , max(0.0f, Dot9(FA22, DSub))}; float DetA = A[0] * A[3] - A[1] * A[1]; if (DetA < 0.0f) { A[1] = 0; DetA = A[0] * A[3]; } float2 b = { Dot9(FB1, DSub), Dot9(FB2, DSub)}; //float c = Dot9(FC, DSub); // Get the sub-pixel offset // \mu = -A^{-1}b, // the constant parameter s = c - \frac{\mu^{T}A\mu}{2} is ignored float2 SubpixelOffset = 0.0f; if (abs(DetA) > 1e-9) { float2 Mu = { -(A[3] * b[0] - A[1] * b[1]) / DetA, -(A[0] * b[1] - A[1] * b[0]) / DetA }; SubpixelOffset = lerp(0, Mu, length2(Mu) < 1); } return SubpixelOffset; } #endif #define MAX_HEAP_SIZE 4 #if MAX_HEAP_SIZE != 4 #error Support up to max heap size of 4 only #endif struct FMaxHeap { float Heap[MAX_HEAP_SIZE]; uint Index[MAX_HEAP_SIZE]; }; FMaxHeap CreateMaxHeap() { FMaxHeap MaxHeap; UNROLL for (int i = 0; i < MAX_HEAP_SIZE; ++i) { MaxHeap.Heap[i] = MaxHalfFloat; MaxHeap.Index[i] = 0; } return MaxHeap; } float GetMax(in float Array[MAX_HEAP_SIZE]) { return Array[0]; } void _Swap(inout float Array[MAX_HEAP_SIZE], uint i, uint j) { float TmpValue = Array[i]; Array[i] = Array[j]; Array[j] = TmpValue; } void _Swap(inout uint Array[MAX_HEAP_SIZE], uint i, uint j) { uint TmpValue = Array[i]; Array[i] = Array[j]; Array[j] = TmpValue; } void Insert(inout FMaxHeap MaxHeap, float Value, int Index) { // Insert only when the value is smaller than the max value of the max heap if (Value < GetMax(MaxHeap.Heap)) { // 1. replace the value at the head MaxHeap.Heap[0] = Value; MaxHeap.Index[0] = Index; // 2. Find max and swap to the head float MaxValue; int MaxIndex; Max4(float4(MaxHeap.Heap[0], MaxHeap.Heap[1], MaxHeap.Heap[2], MaxHeap.Heap[3]), MaxValue, MaxIndex); if (MaxIndex != 0) { _Swap(MaxHeap.Heap, 0, MaxIndex); _Swap(MaxHeap.Index, 0, MaxIndex); } } } void Insert(inout FMaxHeap MaxHeap, float4 Values, int4 Indices) { UNROLL for (int i = 0; i < 4; ++i) { Insert(MaxHeap, Values[i], Indices[i]); } } void Sort4(float Values[MAX_HEAP_SIZE], inout uint4 Indices) { uint Index[MAX_HEAP_SIZE] = {0, 1, 2, 3}; // Optimal number of comparison is 5 if (Values[Index[0]] < Values[Index[1]]) { _Swap(Index, 0, 1); } if (Values[Index[2]] < Values[Index[3]]) { _Swap(Index, 2, 3); } if (Values[Index[0]] < Values[Index[2]]) { _Swap(Index, 0, 2); } if (Values[Index[1]] < Values[Index[3]]) { _Swap(Index, 1, 3); } if (Values[Index[1]] < Values[Index[2]]) { _Swap(Index, 1, 2); } Indices = uint4(Index[0], Index[1], Index[2], Index[3]); } // based on the target position, and the shift from the last mip, we fetch FQuadricFittingContext GetQuadricFittingContext(int2 Position, int kth = 0) { kth = clamp(kth, 0, MAX_HEAP_SIZE - 1); // get the minimal distance and the surrounding FFindMinContext Context = (FFindMinContext)0; BRANCH if (kth == 0) { Context.TextureOffset = 3;// starting to have (0,0) as the minimal value Min4(DistanceTextures_3[Position], Context.Value, Context.Index); UpdateMin4Index(1, DistanceTextures_1[Position], Context); UpdateMin4Index(2, DistanceTextures_2[Position], Context); UpdateMin4Index(4, DistanceTextures_4[Position], Context); UpdateMin4Index(0, DistanceTextures_0[Position], Context); UpdateMin4Index(5, DistanceTextures_5[Position], Context); int MinIndex = 0; float MinValue = DistanceTextures_6[Position].x; if (MinValue < Context.Value) { Context.TextureOffset = 6; Context.Value = MinValue; Context.Index = MinIndex; } } else { //TODO: assign the first distance texture values FMaxHeap MaxHeap = CreateMaxHeap(); Insert(MaxHeap, DistanceTextures_3[Position], 3 * 4 + int4(0, 1, 2, 3)); Insert(MaxHeap, DistanceTextures_1[Position], 1 * 4 + int4(0, 1, 2, 3)); Insert(MaxHeap, DistanceTextures_2[Position], 2 * 4 + int4(0, 1, 2, 3)); Insert(MaxHeap, DistanceTextures_4[Position], 4 * 4 + int4(0, 1, 2, 3)); Insert(MaxHeap, DistanceTextures_0[Position], 0 * 4 + int4(0, 1, 2, 3)); Insert(MaxHeap, DistanceTextures_5[Position], 5 * 4 + int4(0, 1, 2, 3)); Insert(MaxHeap, DistanceTextures_6[Position].x, 6 * 4); uint4 Max4Index; Sort4(MaxHeap.Heap, Max4Index); Context.TextureOffset = Max4Index[kth] / 4; Context.Index = Max4Index[kth] % 4; Context.Value = GetMax(MaxHeap.Heap); } FQuadricFittingContext QuadContext; QuadContext.Offset = Shifts[Context.TextureOffset * 4 + Context.Index]; #if !ESTIMATE_SUBPIXELOFFSET QuadContext.Values = Context.Value; #else int2 ShiftedPosition = Position + QuadContext.Offset; QuadContext.Values[0][0] = GetOffsets(ShiftedPosition, int2(-1, -1)); QuadContext.Values[0][1] = GetOffsets(ShiftedPosition, int2( 0, -1)); QuadContext.Values[0][2] = GetOffsets(ShiftedPosition, int2( 1, -1)); QuadContext.Values[1][0] = GetOffsets(ShiftedPosition, int2(-1, 0)); QuadContext.Values[1][1] = Context.Value; QuadContext.Values[1][2] = GetOffsets(ShiftedPosition, int2( 1, 0)); QuadContext.Values[2][0] = GetOffsets(ShiftedPosition, int2(-1, 1)); QuadContext.Values[2][1] = GetOffsets(ShiftedPosition, int2( 0, 1)); QuadContext.Values[2][2] = GetOffsets(ShiftedPosition, int2( 1, 1)); #endif return QuadContext; } float GetTotalVariation(int2 Position, FQuadricFittingContext Context) { int2 ShiftedPosition = Position + Context.Offset; float TotalVariation = 0.0f; for (int i = -2; i < 2; ++i) { for (int j = -2; j < 2; ++j) { float vij = GetOffsets(ShiftedPosition, int2( i, j)); float vi_1j = GetOffsets(ShiftedPosition, int2( i + 1, j)); float vij_1 = GetOffsets(ShiftedPosition, int2( i, j + 1)); TotalVariation += abs(vi_1j - vij) + abs(vij_1 - vij); } } for (int j = -2; j < 2; ++j) { float vij = GetOffsets(ShiftedPosition, int2(2, j)); float vij_1 = GetOffsets(ShiftedPosition, int2(2, j + 1)); TotalVariation += abs(vij - vij_1); } for (int i = -2; i < 1; ++i) { float vij = GetOffsets(ShiftedPosition, int2( i, 2)); float vi_1j = GetOffsets(ShiftedPosition, int2( i + 1, 2)); TotalVariation += abs(vij - vi_1j); } return TotalVariation; } FPixelOffset GetQuadricFittingOffset(FQuadricFittingContext Context) { FPixelOffset Offset; #if ESTIMATE_SUBPIXELOFFSET float2 SubpixelOffset = EstimateSubpixelOffset(Context.Values); Offset.xy = Context.Offset + SubpixelOffset; #else Offset.xy = Context.Offset; #endif Offset.Dist = Context.Values[1][1]; return Offset; } #endif int2 GetShiftedPosition(int2 Position, int2 Shift) { return Position + Shift; } float2 GetBufferUVFromPosition(float2 Position, int MipLevel) { return (Position + 0.5f) / ((uint2)TargetViewport_Extent >> MipLevel); } // Error Normalization. // to have a similar error level between euclidean e.g. for color // C0 = (0.58f, 0.46f, 0.28f), C1 = (0.28f, 0.96f, 0.43f) // the color difference of euclidean with gamma corrected D_eu_g // D_eu_g(C0, C1) = 0.38f // the color difference of DeltaE_CIE2000 has // DeltaE(C0,C1) = 27.71 // To have the error on the same level, we multiply by 0.53f/ 37.85f = 1/70.78f, which is an experimental value // of running 1e6 random pair of colors to get the mean of different measures where // Mean_eu_g = 0.53f, Mean_DeltaE = 37.85f; static float NormalizeDeltaEAndEuclideanDistanceToSameMean = 0.0141f; #define NormalizeDeltaE2000(X) (NormalizeDeltaEAndEuclideanDistanceToSameMean*X) // rgb is in linear space float GetDeltaE(float3 LinearRGB0, float3 LinearRGB1, bool bNormalizeToEuclideanMean = true) { float3 Lab0 = LinearRGB_2_LAB(LinearRGB0); float3 Lab1 = LinearRGB_2_LAB(LinearRGB1); float DeltaE = DeltaE_CIE2000(Lab0, Lab1); if (bNormalizeToEuclideanMean) { DeltaE = NormalizeDeltaE2000(DeltaE); } return DeltaE; } float GetDistance(float LumA, float LumB, int2 shift) { float distance = 0; #if DISTANCE_TYPE == DISTANCE_L1 return abs(LumA - LumB); #elif DISTANCE_TYPE == DISTANCE_L2 return Square(LumA - LumB); #elif DISTANCE_TYPE == DISTANCE_LOG_L1 return log2(2 + abs(LumA - LumB)); #elif DISTANCE_TYPE == DISTANCE_LOG_REGULARIZED_L1 return log2(2 + abs(LumA - LumB)) * (2 - exp(-0.02 * length2(shift))); #else #error Unsupported dinstance type between two pixels, please implement. return 0.0f; #endif } float GetFinalMipDistance(float LumA, float LumB, int2 shift) { return log2(2 + abs(LumA - LumB)) * (2 - exp(-0.02 * length2(shift))); } float GetDistance(float3 A, float3 B, int2 shift) { #if APPLY_SIMPLIFIED_GAMMA A = pow(abs(A), 1 / 2.2f); B = pow(abs(B), 1 / 2.2f); #endif #if DISTANCE_METRICS == METRICS_LUMINANCE float LumA = Luminance(A); float LumB = Luminance(B); #elif DISTANCE_METRICS == METRICS_EUCLIDEAN float LumA = length(A - B); float LumB = 0; #elif DISTANCE_METRICS == METRICS_PERCEPTION float LumA = GetDeltaE(A, B); float LumB = 0; #endif return GetDistance(LumA, LumB, shift); } float GetFinalMipDistance(float3 A, float3 B, int2 shift) { #if APPLY_SIMPLIFIED_GAMMA A = pow(abs(A), 1 / 2.2f); B = pow(abs(B), 1 / 2.2f); #endif #if DISTANCE_METRICS == METRICS_LUMINANCE float LumA = Luminance(A); float LumB = Luminance(B); #elif DISTANCE_METRICS == METRICS_EUCLIDEAN float LumA = length(A - B); float LumB = 0; #elif DISTANCE_METRICS == METRICS_PERCEPTION float LumA = GetDeltaE(A, B); float LumB = 0; #endif return GetFinalMipDistance(LumA, LumB, shift); } #if TEMPORAL_REPROJECTION_ALIGN || TEMPORAL_REPROJECTION_MERGE || TEMPORAL_REPROJECTION_RESOLVE || TEMPORAL_HIGHFREQ_REJECT Texture2D PixelOffsetTexture; Texture2D SourceTexture; Texture2D TargetTexture; SamplerState SharedTextureSampler; uint MipLevel; FPixelOffset GetPixelOffsetFromLowerMip(float2 Position, uint MipDiff) { uint MipPosMultipler = 1u << (MipDiff); uint LowerMipLevel = MipLevel + MipDiff; float2 LowerMipPosition = 0.0f; BRANCH if (MipDiff == 0) { LowerMipPosition = Position; } else { // Keep the reprojection border of the coarse level close to zero LowerMipPosition = (Position - 1.0) / (float)MipPosMultipler - 0.5f; } FPixelOffset Offset = (FPixelOffset)0; float2 Extent = (float2)((uint2)TargetViewport_Extent >> LowerMipLevel); float2 UV = (LowerMipPosition + 0.5f) / Extent; float4 OffsetDist = Texture2DSampleBicubic(PixelOffsetTexture, SharedTextureSampler, UV, Extent, 1.0f / Extent); Offset.xy = MipPosMultipler * OffsetDist.xy; Offset.Dist = OffsetDist.z; #if TOTAL_VARIATION || TEMPORAL_REPROJECTION_RESOLVE || TEMPORAL_HIGHFREQ_REJECT Offset.TV = OffsetDist.w; #else Offset.TV = 1.0f; #endif return Offset; } float3 GetTextureValue(Texture2D Tex, float2 Position) { float2 BufferUV = GetBufferUVFromPosition(Position, MipLevel); return Texture2DSampleLevel(Tex, SharedTextureSampler, BufferUV, 0).rgb; } float3 GetTextureValueWithOffset(Texture2D Tex, float2 Position) { // Get the postion offset from the previous miplevel texture FPixelOffset Offset = GetPixelOffsetFromLowerMip(Position, MIP_DIFF_DELTA); float2 BufferUV = GetBufferUVFromPosition(Position + Offset.xy, MipLevel); return Texture2DSampleLevel(Tex, SharedTextureSampler, BufferUV, 0).rgb; } #endif #if TEMPORAL_PREPASS Texture2D InputTexture; Texture2D AlbedoTexture; Texture2D NormalTexture; RWStructuredBuffer RWVarianceMap; int Iteration; // Channel ranked luminance places the max value to g, and minimal to b // In this way, the luminance is perception independent. The variance of (X,0,0) is the same to (0,X,0) // instead of being smaller. float ChannelRankedLuminance(float3 Value) { int Index = 0; Max3Index(Value, Index); Swap(Value[Index], Value.g); // swap max value to g Min3Index(Value, Index); Swap(Value[Index], Value.b); // swap min value to b return Luminance(Value); } // Note that the radiance, albedo and normal are the mean value. float4 GetMeanFromTextures(int2 Position) { float4 Value = 0; #if VARIANCE_TYPE == RADIANCE_MULTI_CHANNEL Value = InputTexture[Position]; #elif VARIANCE_TYPE == RADIANCE_ALBEDO_NORMAL_SINGLECHANNEL #if RANKED_LUMINANCE_VARIANCE Value.x = ChannelRankedLuminance(InputTexture[Position].rgb); Value.y = ChannelRankedLuminance(AlbedoTexture[Position].rgb); #else Value.x = Luminance(InputTexture[Position].rgb); Value.y = Luminance(AlbedoTexture[Position].rgb); #endif //The length of the average normal indicates the variance //1: All pointing to the same direction //0: Uniform in all directions Value.z = length(NormalTexture[Position].rgb); // Special case handling: // 1. Regions without normal has a zero length, like sky // 2. The first frame if (Value.z == 0.0f || Iteration == 0) { Value.z = 1.0f; } // Alpha channel needs denoising as well. Value.w = InputTexture[Position].a; #else #error "Not implemented" #endif return Value; } [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void TemporalPrepassCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = (int2)DT_ID.xy; int2 MaxPosition = int2(TargetViewport_Extent); if (all(DT_ID.xy < MaxPosition)) { int index = Position.x + Position.y * TargetViewport_Extent.x; float4 Mean = GetMeanFromTextures(Position); #if PREPASS_PHASE == PREPASS_PHASE_INIT FPixelMaterialLightingFingerprint VarianceInfo = (FPixelMaterialLightingFingerprint)0; VarianceInfo.Mean = Mean; RWVarianceMap[index] = VarianceInfo; #elif PREPASS_PHASE == PREPASS_PHASE_UPDATE FPixelMaterialLightingFingerprint VarianceInfo = RWVarianceMap[index]; float4 PreviousMean = VarianceInfo.Mean; float4 CurrentValue = (Mean - PreviousMean) * Iteration + Mean; VarianceInfo.Mean = Mean; VarianceInfo.Var += (CurrentValue - VarianceInfo.Mean)*(CurrentValue - PreviousMean); RWVarianceMap[index] = VarianceInfo; #else #error "Not implemented" #endif } } #endif // TEMPORAL_PREPASS #if PREPASS_GENERATE_TEXTURE RWTexture2D OutputTexture; StructuredBuffer VarianceMap; int Iteration; [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void PrepassGenerateTextureCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = (int2)DT_ID.xy; int2 MaxPosition = int2(TargetViewport_Extent); if (all(DT_ID.xy < MaxPosition)) { int index = Position.x + Position.y * TargetViewport_Extent.x; FPixelMaterialLightingFingerprint VarianceInfo = VarianceMap[index]; // VarianceInfo.Var = sigma^2 * n. // Std of mean = sqrt(sigma^2/n) = sqrt(VarianceInfo.Var/n^2) float4 StandDerivation= sqrt(VarianceInfo.Var) / (Iteration + 1); OutputTexture[Position] = StandDerivation; } } #endif // PREPASS_GENERATE_TEXTURE #if TEMPORAL_FEATURE_FUSION //Use Albedo, Normal, and Radiance Texture to create another feature vector // such that the feature vector leads to the best motion vector estimation. Texture2D AlbedoTexture_0; Texture2D AlbedoTexture_1; Texture2D NormalTexture_0; Texture2D NormalTexture_1; Texture2D RadianceTexture_0; Texture2D RadianceTexture_1; StructuredBuffer VarianceMap_0; StructuredBuffer VarianceMap_1; Texture2D LastDenoisedRadiance; SamplerState SharedTextureSampler; RWTexture2D OutputTexture_0; RWTexture2D OutputTexture_1; #define SOURCE(x) x##_0[Position] #define TARGET(x) x##_1[Position] #define SOURCE_BUFFER(B) B##_0[Position.x+int(Position.y*TargetViewport_Extent.x)] #define TARGET_BUFFER(B) B##_1[Position.x+int(Position.y*TargetViewport_Extent.x)] float4 Fuse(float4 Albedo, float4 Normal, float4 Radiance, float4 History, FPixelMaterialLightingFingerprint Fingerprint) { float4 combined = 0; //TODO: Derive good feature fusion algorithm. combined.rgb = sqrt(Fingerprint.Var.rgb / (Fingerprint.Mean.w + 1)); return combined; } float4 FuseSource(int2 Position) { float4 Albedo = SOURCE(AlbedoTexture); float4 Normal = SOURCE(NormalTexture); float4 Radiance = SOURCE(RadianceTexture); float4 History = LastDenoisedRadiance[Position]; FPixelMaterialLightingFingerprint Fingerprint = SOURCE_BUFFER(VarianceMap); return Fuse(Albedo, Normal, Radiance, History,Fingerprint); } float4 FuseTarget(int2 Position) { float4 Albedo = TARGET(AlbedoTexture); float4 Normal = TARGET(NormalTexture); float4 Radiance = TARGET(RadianceTexture); float4 History = -1.0f; FPixelMaterialLightingFingerprint Fingerprint = TARGET_BUFFER(VarianceMap); return Fuse(Albedo, Normal, Radiance, History, Fingerprint); } [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void TemporalFeatureFusionCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = DT_ID.xy; int2 MaxPosition = (int2)TargetViewport_Extent; if (all(Position < MaxPosition)) { SOURCE(OutputTexture) = FuseSource(Position); TARGET(OutputTexture) = FuseTarget(Position); } } #endif // TEMPORAL_FEATURE_FUSION #if TEMPORAL_REPROJECTION_ALIGN [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void ReprojectionAlignCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = DT_ID.xy; int SliceId = DT_ID.z; uint3 MaxPosition = uint3(((uint2)TargetViewport_Extent.xy) >> MipLevel, K_NUM_OF_TEXTURES_PER_PASS); if (all(DT_ID.xyz < MaxPosition)) { float3 Target = GetTextureValue(TargetTexture, Position)*View.PreExposure; float4 Distances = 0.0f; UNROLL for (int i = 0; i < K_NUM_OF_SHIFTS_PER_SLICE; ++i) { int2 Shift = Shifts[SliceId * K_NUM_OF_SHIFTS_PER_SLICE + i]; int2 ShiftPosition = GetShiftedPosition(Position, Shift); float3 Source = GetTextureValueWithOffset(SourceTexture, ShiftPosition)*View.PreExposure; if (MipLevel != 0) { Distances[i] = GetDistance(Target, Source, Shift); } else { Distances[i] = GetFinalMipDistance(Target, Source, Shift); } } SaveOffsets(Position, SliceId * K_NUM_OF_SHIFTS_PER_SLICE, Distances); } } #endif // TEMPORAL_REPROJECTION_ALIGN #if TEMPORAL_REPROJECTION_BLUR Texture2D InputTexture; // Previous accumulation RWTexture2D OutputTexture; // new observation SamplerState SharedTextureSampler; uint MipLevel; float4 Get3x3Blur(int2 Position) { float2 BufferUV = GetBufferUVFromPosition(Position, MipLevel); float2 HalfPixel = 0.5f * (1u << MipLevel) * TargetViewport_ExtentInverse; #if 0 //Sampling pattern and weights // 1 1 // 4 // 1 1 float4 Sum = Texture2DSampleLevel(InputTexture, SharedTextureSampler, BufferUV, 0) * 4; Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + float2(-HalfPixel.x, -HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + float2(HalfPixel.x, -HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + float2(-HalfPixel.x, HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + float2(HalfPixel.x, HalfPixel.y)), 0); return Sum / 8.0f; #elif DIRECT_COPY return InputTexture[Position]; #else float4 Sum = 0; Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(-HalfPixel.x, -HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(-HalfPixel.x, 0)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(-HalfPixel.x, HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(0, -HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(0, 0)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(0, HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(HalfPixel.x, -HalfPixel.y)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(HalfPixel.x, 0)), 0); Sum += Texture2DSampleLevel(InputTexture, SharedTextureSampler, (BufferUV + 2.0 * float2(HalfPixel.x, HalfPixel.y)), 0); return Sum / 9.0f; #endif } [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void ReprojectionBlurCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = (int2)DT_ID.xy; int2 MaxPosition = int2(((uint2)TargetViewport_Extent) >> MipLevel); if (all(Position.xy < MaxPosition)) { float4 Blur = Get3x3Blur(Position); OutputTexture[Position] = Blur; } } #endif // TEMPORAL_REPROJECTION_ALIGN #if TEMPORAL_REPROJECTION_MERGE RWTexture2D RWPixelOffsetTexture; uint PatchId; uint PatchCount; // For a three level mip chain and a delta of MIP_DIFF_DELTA. // fetch the patch. E.g., // 0-0-0 fetch the patch with the minimal distance // 0-1-0 fetch the second best for the second mip level uint GetTheKthPatchBasedOnMipAndPatchId() { uint Mip2PatchIndex = PatchId % 4; uint Mip0PatchIndex = PatchId / 4; const uint TopMipLevel = 0; uint TheKthPatchForMipLevel = lerp( lerp(0, // All others Mip2PatchIndex, MipLevel == (TopMipLevel + MIP_DIFF_DELTA)), // 2nd mip level Mip0PatchIndex, MipLevel == TopMipLevel ); // Most fine grain level return TheKthPatchForMipLevel; } [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void ReprojectionMergeCS(uint3 DT_ID : SV_DispatchThreadID) { // Keep the debug here as the reproject still needs some work around borders for better history reuse. bool DebugMotionVector = false; int2 Position = DT_ID.xy; int2 MaxPosition = int2(((uint2)TargetViewport_Extent) >> MipLevel); if (all(Position < MaxPosition)) { uint Kth = GetTheKthPatchBasedOnMipAndPatchId(); FQuadricFittingContext Context = GetQuadricFittingContext(Position, Kth); // Add up the old offset with the new one. FPixelOffset OffsetFromLowerMip = GetPixelOffsetFromLowerMip(Position, MIP_DIFF_DELTA); // the offset is from target to source FPixelOffset Offset = GetQuadricFittingOffset(Context); float4 PixelDiff = float4(Offset.xy + OffsetFromLowerMip.xy, Offset.Dist, length(Offset.xy + OffsetFromLowerMip.xy)); #if TOTAL_VARIATION float TotalVariation = GetTotalVariation(Position + PixelDiff.xy, Context); PixelDiff.w = TotalVariation; #else PixelDiff.w = 1.0f; #endif if (!DebugMotionVector) { RWPixelOffsetTexture[Position] = PixelDiff; } else { if (MipLevel >= 6) { RWPixelOffsetTexture[Position] = PixelDiff; } else { RWPixelOffsetTexture[Position] = float4(OffsetFromLowerMip.xy, OffsetFromLowerMip.Dist, length(OffsetFromLowerMip.xy)); } } } } #endif // TEMPORAL_REPROJECTION_MERGE #if TEMPORAL_HIGHFREQ_REJECT RWTexture2D OutputTexture; float HighFrequencyCutoffDeltaE; [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void TemporalHighFrequencyRejectCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = DT_ID.xy; int2 MaxPosition = (int2)TargetViewport_Extent; if (all(Position < MaxPosition)) { FPixelOffset Offset = GetPixelOffsetFromLowerMip(Position, 0); float2 Pos = Position + Offset.xy; float2 UV = (Pos + 0.5f) * TargetViewport_ExtentInverse; float4 History_Warped = Texture2DSampleBicubic(SourceTexture, SharedTextureSampler, UV, TargetViewport_Extent, TargetViewport_ExtentInverse); float4 Target = TargetTexture[Position]; //calculate the distance between warped source and target const bool bNormalizeDeltaE = true; float dENormalized = GetDeltaE(History_Warped.rgb*View.PreExposure, Target.rgb*View.PreExposure, bNormalizeDeltaE); // since the warping of the source image is based on the distance of a region with weights // it is a matching based on low frequency information. High frequency detail is not well // utilized. So in this CS, we try to use the high frequency information to correct the blending // factor per pixel. E.g., when albedo is used, we expect a high matching. // TODO: Explore different buffers. OutputTexture[Position] = float4(dENormalized, dENormalized >= NormalizeDeltaE2000(HighFrequencyCutoffDeltaE), Luminance(History_Warped.rgb), Luminance(Target.rgb)); } } #endif // TEMPORAL_HIGHFREQ_REJECT #if TEMPORAL_REPROJECTION_RESOLVE Texture2D HighFrequencyRejectMap; RWTexture2D OutputTexture; float Alpha; float Kappa; float Eta; float HighFrequencyCutoffDeltaE; uint PatchId; uint PatchCount; [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void TemporalResolveCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = DT_ID.xy; int2 MaxPosition = (int2)TargetViewport_Extent; if (all(Position < MaxPosition)) { FPixelOffset Offset = GetPixelOffsetFromLowerMip(Position, 0); float2 Pos = Position +Offset.xy; float2 UV = (Pos + 0.5f) * TargetViewport_ExtentInverse; float4 Sampling = Texture2DSampleBicubic(SourceTexture, SharedTextureSampler, UV, TargetViewport_Extent, TargetViewport_ExtentInverse); float m = max(Kappa * (Offset.Dist - Eta), 0.0f); m = 2 * m / (1 + m); float FinalAlpha = clamp(Alpha * (1 - m) * Offset.TV, 0, 1); float dEHighFrequency = HighFrequencyRejectMap[Position].x; // Determine the final alpha based on perception error // 1. if the high frequency difference is larger than 10dE reject // 2. if the difference is smaller than the high frequency cutoff, use the weight based on low frequency // 3. otherwise, we move the target color to the source by a distance at most 0.5 dE // TODO: lerp in Lab space instead of linear RGB space? FinalAlpha = lerp(FinalAlpha, min(FinalAlpha, NormalizeDeltaE2000(0.5f) / dEHighFrequency), dEHighFrequency >= NormalizeDeltaE2000(HighFrequencyCutoffDeltaE)); FinalAlpha = lerp(FinalAlpha, 0, dEHighFrequency >= NormalizeDeltaE2000(10.0f)); float4 Accumulation = 0.0f; float Weight = 0.0f; float4 TargetValue = TargetTexture[Position]; BRANCH if (PatchCount == 1) { // Use exponential moving to low pass the signal to create more // temporal stable results. Accumulation = (1 - FinalAlpha) * TargetValue + FinalAlpha * Sampling; Weight = FinalAlpha; } else { // When we have multiple patches, use bilaterial filter to combine them // TODO: improve high frequency information for this mode. Accumulation = TargetValue; Weight = (PatchId == 0) ? 1 : Accumulation.w; Accumulation += FinalAlpha * Sampling; Weight += FinalAlpha; if (PatchId + 1 == PatchCount) { Accumulation /= Weight; } } OutputTexture[Position] = float4(max(Accumulation.xyz, 0.0f), Weight); } } #endif // TEMPORAL_REPROJECTION_RESOLVE // TODO: Ablation test float GetWeightMultiplier(float SourceTV, float TargetSourceTV) { // Use less history if total variation is larger than reference float delta = -(TargetSourceTV - SourceTV); float depth = 0.2f; return 1 / (1 + exp(-10.0 * delta)) * depth + (1 - depth / 2.0); } #if MOTION_VECTOR_SUBTRACT RWTexture2D Minuend; Texture2D Subtrahend; uint MipLevel; [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void MotionVectorSubtractCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = DT_ID.xy; int2 MaxPosition = int2(((uint2)TargetViewport_Extent) >> MipLevel); if (all(Position < MaxPosition)) { float4 M = Minuend[Position]; float4 S = Subtrahend[Position]; float4 M_S = float4(M.x - S.x, M.y - S.y, M.z, GetWeightMultiplier(S.w, M.w)); // Do not estimate the TV at the border when history is not available const uint NumOfIgnoredBorderPixels = 12; if (any(Position > TargetViewport_Extent - NumOfIgnoredBorderPixels) || any(Position - NumOfIgnoredBorderPixels < float2(0, 0))) { M_S.w = 1.0f; } Minuend[Position] = M_S; } } #endif // MOTION_VECTOR_SUBTRACT #if VISUALIZE_MOTIONVECTOR // h = [0, 1], s = [0 ,1], v = [0, 1] // h: hue, // s: saturate // v: value float3 hsv2rgb(float h, float s, float v) { h = 360.0 * h; float C = v * s; float X = C * (1 - abs( (h/60) % 2 - 1)); float m = v - C; float3 RGB = 0; if (h >= 0 && h < 60) { RGB.r = C; RGB.g = X; } else if (h >= 60 && h < 120) { RGB.r = X; RGB.g = C; } else if (h >= 120 && h < 180) { RGB.g = C; RGB.b = X; } else if (h >= 180 && h < 240) { RGB.g = X; RGB.b = C; } else if (h >= 240 && h < 300) { RGB.r = X; RGB.b = C; } else if (h >= 300 && h < 360) { RGB.r = C; RGB.b = X; } RGB += m; return RGB; } //:adapted from https://www.shadertoy.com/view/MsS3Wc by iq float3 hsv2rgb_smooth(float3 c) { float3 rgb = clamp(abs( (c.x * 6.0 + float3(0.0, 4.0, 2.0)) % 6.0 - 3.0) - 1.0, 0.0, 1.0); rgb = rgb * rgb * (3.0 - 2.0 * rgb); // cubic smoothing return c.z * lerp(float3(1.0, 1.0, 1.0), rgb, c.y); } Texture2D TemporalDenoisingMotionVector; Texture2D DenoisedTexture; #define VISUALIZE_VECTOR 1 #define VISUALIZE_COLOR_ENCODING 1 #define VECTOR_GRID_LENGTH 32 #define COLOR_METER_LENGTH 50 void VisualizePathTracingMotionVector( in noperspective float2 UV : TEXCOORD0, out float4 OutColor : SV_Target0 ) { float2 BufferSize = View.BufferSizeAndInvSize.xy; int3 TexCoord = int3(UV * BufferSize - View.ViewRectMin.xy, 0); float4 Velocity = TemporalDenoisingMotionVector.Load(TexCoord); // draw the denoised scene OutColor.rgb = DenoisedTexture.Load(TexCoord).rgb * View.PreExposure; #if VISUALIZE_COLOR_ENCODING float s, h, v; if (length(Velocity.xy) >= 0.001) // ignore small velocity color encoding { #if 0 s = clamp(lengthFast(float3(Velocity.xy, 0.0f)), 0.0f, 50.0f) / 50.0f; h = atan2(Velocity.y, Velocity.x) / 3.1415926f * 0.5f + 0.5f; v = 1.0f; OutColor = float4(hsv2rgb(h, s, v), 1.0f);// float4(s, h, Velocity.b, 1.0f); OutColor.rgb = pow(1 - OutColor.rgb, 1 / 2.2f); #else v = pow(clamp(length(float3(Velocity.xy, 0.0f)), 0.0f, 50.0f) / 50.0f, 1 / 2.2f); h = atan2(Velocity.y, -Velocity.x) / 3.1415927f; h = h < 0 ? ((h + 2) / 2) : h / 2.0; s = 1.0f; OutColor = float4(hsv2rgb_smooth(float3(h, s, v)), 1.0f);// float4(s, h, Velocity.b, 1.0f); OutColor.rgb = pow(OutColor.rgb, Velocity.z == 0? 1.0f: Velocity.z); #endif } else { OutColor.rgb = 0.0f; } // add motion color meter if (length(TexCoord.xy - COLOR_METER_LENGTH) < COLOR_METER_LENGTH) { float2 Vector = TexCoord.xy - COLOR_METER_LENGTH; #if 0 s = length(float3(Vector, 0)) / COLOR_METER_LENGTH; h = atan2(-Vector.y, -Vector.x) / 3.1415927f * 0.5f + 0.4999f; v = 1.0f; OutColor = float4(hsv2rgb(h, s, v), 1.0f);// float4(s, h, Velocity.b, 1.0f); OutColor.rgb = pow(1 - OutColor.rgb, 1 / 2.2f); #else v = pow(length(float3(Vector, 0)) / COLOR_METER_LENGTH, 1 / 2.2f); h = atan2(-Vector.y, Vector.x) / 3.1415927f; h = h < 0 ? ((h + 2) / 2) : h / 2.0; s = 1.0f; OutColor = float4(hsv2rgb_smooth(float3(h, s, v)), 1.0f);// float4(s, h, Velocity.b, 1.0f); OutColor.rgb = pow(OutColor.rgb, 1.0f); #endif } #endif #if VISUALIZE_VECTOR int2 P0 = (TexCoord.xy / VECTOR_GRID_LENGTH) * VECTOR_GRID_LENGTH + VECTOR_GRID_LENGTH / 2; Velocity = TemporalDenoisingMotionVector.Load(int3(P0, 0)); float2 P1 = (float2)P0 + Velocity.xy; float2 N = normalize(float2(Velocity.y, -Velocity.x)); float2 P2 = (float2)TexCoord.xy; #if 0 // plot actual velocity float Dist = abs(dot(N, P2 - P0)); float Lambda = dot(P2 - P0, P1 - P0) / dot(Velocity.xy, Velocity.xy); float P02P1Length = length(Velocity.xy); if (P02P1Length > 0.001) { if (Dist <= 3 && Lambda >= -0.1 && Lambda <= 1.1) { if (Dist <= 1.0) { OutColor = lerp(float4(0.5f, 0.5f, 0.5f, 1.0f), float4(1.0f, 0.0f, 0.0f, 1.0f), Lambda); } if (length(P2-P0) <= 2) { OutColor = 0.6f; } if (length(P2-P1)<=2) { OutColor = float4(1.0, 0.0, 0.0, 1.0f); } } } #else float3 LineColor = float3(0.7, 0.7, 0.7); // points into the movement direction, movement direction is the inverse of the reprojection velocity. float2 PixelDirection = -Velocity.xy; float2 PixelPosAtTileCenter = P0; float2 PixelPos = P2; // arrow { float2 PerpPixelDirection = float2(PixelDirection.y, -PixelDirection.x); float2 DirectionInTile = PixelPos - PixelPosAtTileCenter; float DistOnLine = dot(normalize(-PixelDirection), DirectionInTile) + length(PixelDirection); bool bArrowHead = DistOnLine < 8; float LocalThickness = 1 + (frac(DistOnLine / 8) * 8) * 0.25f; float PerpDirectionMask = saturate(LocalThickness - abs(dot(normalize(PerpPixelDirection), DirectionInTile))); float DirectionMask = saturate(length(PixelDirection) - length(DirectionInTile)); float3 LineMask = PerpDirectionMask * DirectionMask; OutColor.rgb = lerp(OutColor.rgb, LineColor, LineMask); } // previous pos is a dot { float3 DotColor = float3(0, 1, 0); // PixelPos of the previous position float2 PreviousPixelPos = PixelPosAtTileCenter - PixelDirection; float Dist = length(PreviousPixelPos - PixelPos); OutColor.rgb = lerp(OutColor.rgb, LineColor, saturate(3 - Dist)); OutColor.rgb = lerp(OutColor.rgb, 0, saturate(1.5f - Dist)); } #endif #endif } #endif #if VISUALIZE_WARPING Texture2D TemporalDenoisingMotionVector; Texture2D DenoisedTexture; Texture2D SourceTexture; SamplerState SharedTextureSampler; void FVisualizeWarpingPS( in noperspective float2 UV : TEXCOORD0, out float4 OutColor : SV_Target0) { float2 BufferSize = View.BufferSizeAndInvSize.xy; int3 TexCoord = int3(UV * BufferSize - View.ViewRectMin.xy, 0); float4 Velocity = TemporalDenoisingMotionVector.Load(TexCoord); float2 Pos = TexCoord.xy + Velocity.xy; float2 SourceUV = (Pos + 0.5f + View.ViewRectMin.xy) * TargetViewport_ExtentInverse; float4 Sampling = Texture2DSampleBicubic(SourceTexture, SharedTextureSampler, SourceUV, TargetViewport_Extent, TargetViewport_ExtentInverse); OutColor.rgb = Sampling.rgb * View.PreExposure; OutColor.a = 1.0f; } #endif #if SPATIAL_DENOISING Texture2D InputTexture; Texture2D InputNormal; Texture2D InputAlbedo; RWTexture2D OutputTexture; [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void SpatialDenoiserCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = DT_ID.xy; int2 MaxPosition = (int2)TargetViewport_Extent; if (all(Position < MaxPosition)) { OutputTexture[Position] = InputTexture[Position]; } } #endif // SPATIAL_DENOISING #if PREPROCESS_BUFFER RWTexture2D NormalTexture; float Width; float Height; [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, 1)] void ConvertWorldSpaceNormalToCameraSpaceCS(uint3 DT_ID : SV_DispatchThreadID) { int2 Position = DT_ID.xy; int2 MaxPosition = int2(Width, Height); if (all(Position < MaxPosition)) { float4 Normal = float4(NormalTexture[Position].xyz, 0.0f); NormalTexture[Position] = float4(mul(Normal, View.TranslatedWorldToCameraView).xyz, 0.0f); } } #endif