1600 lines
43 KiB
HLSL
1600 lines
43 KiB
HLSL
// 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<float4> RWDistanceTextures_0;
|
|
RWTexture2D<float4> RWDistanceTextures_1;
|
|
RWTexture2D<float4> RWDistanceTextures_2;
|
|
RWTexture2D<float4> RWDistanceTextures_3;
|
|
RWTexture2D<float4> RWDistanceTextures_4;
|
|
RWTexture2D<float4> RWDistanceTextures_5;
|
|
RWTexture2D<float4> RWDistanceTextures_6;
|
|
|
|
Texture2D<float4> DistanceTextures_0;
|
|
Texture2D<float4> DistanceTextures_1;
|
|
Texture2D<float4> DistanceTextures_2;
|
|
Texture2D<float4> DistanceTextures_3;
|
|
Texture2D<float4> DistanceTextures_4;
|
|
Texture2D<float4> DistanceTextures_5;
|
|
Texture2D<float4> 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<float4> PixelOffsetTexture;
|
|
Texture2D<float4> SourceTexture;
|
|
Texture2D<float4> 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<float4> InputTexture;
|
|
Texture2D<float4> AlbedoTexture;
|
|
Texture2D<float4> NormalTexture;
|
|
RWStructuredBuffer<FPixelMaterialLightingFingerprint> 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<float4> OutputTexture;
|
|
StructuredBuffer<FPixelMaterialLightingFingerprint> 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<float4> AlbedoTexture_0;
|
|
Texture2D<float4> AlbedoTexture_1;
|
|
|
|
Texture2D<float4> NormalTexture_0;
|
|
Texture2D<float4> NormalTexture_1;
|
|
|
|
Texture2D<float4> RadianceTexture_0;
|
|
Texture2D<float4> RadianceTexture_1;
|
|
|
|
StructuredBuffer<FPixelMaterialLightingFingerprint> VarianceMap_0;
|
|
StructuredBuffer<FPixelMaterialLightingFingerprint> VarianceMap_1;
|
|
|
|
|
|
Texture2D<float4> LastDenoisedRadiance;
|
|
|
|
SamplerState SharedTextureSampler;
|
|
|
|
RWTexture2D<float4> OutputTexture_0;
|
|
RWTexture2D<float4> 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<float4> InputTexture; // Previous accumulation
|
|
RWTexture2D<float4> 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<float4> 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<float4> 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<float4> HighFrequencyRejectMap;
|
|
RWTexture2D<float4> 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<float4> Minuend;
|
|
Texture2D<float4> 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<float4> TemporalDenoisingMotionVector;
|
|
Texture2D<float4> 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<float4> TemporalDenoisingMotionVector;
|
|
Texture2D<float4> DenoisedTexture;
|
|
Texture2D<float4> 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<float4> InputTexture;
|
|
Texture2D<float4> InputNormal;
|
|
Texture2D<float4> InputAlbedo;
|
|
RWTexture2D<float4> 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<float4> 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
|