I recently implemented Mitchell-Netravali filtering in my path-tracer with 16x (4x4) temporal anti-aliasing. PBR's demonstration image shows it should give significantly better results than the box filter, but my test pictures barely show any improvement at all (and the sharpening in Mitchell-Netravali actually highlights jaggies that would have been blurred out by the box filter).
Is this filter designed for a higher sampling rate? My samples are spread over an even sub-pixel grid, and I'm using $B$ and $C$ constants on the line $B + 2C = 1$. I'm also using the implementation recommended by PBR.
Function for $f(x, y)$:
// Small functions returning the Mitchell-Netravali filter coefficient
// for a given 2D point
// Mitchell-Netravali interpolates sparse samples much more effectively
// than naive filtering (=> applying a box filter), allowing for more
// effective anti-aliasing at lower sampling rates
// Core filter polynomial
float MitchellNetravaliPerAxis(float filtVal)
{
// Mitchell-Netravali parameters
// Mitchell and Netravali recommend parameters of the
// form [B + 2C = 1]
const float B = 0.35f;
const float C = 0.325f;
// Powers of [filtVal] used during the filtering process
float filtValCub = filtVal * filtVal * filtVal;
float filtValSqr = filtVal * filtVal;
// Polynomial filter application
if (filtVal > 1.0f)
{
// Filtering for values greater-than/equal-to one but less than two
return ((B * -1.0f) - (6.0f * C)) * filtValCub +
((6.0f * B) + (30.0f * C)) * filtValSqr +
((-12.0f * B) - (48.0f * C)) * filtVal +
((8.0f * B) + (24.0f * C));
}
else
{
// Filtering for other possible values (less than one; values greater than
// two are cancelled out by the division by pixel radius in [MitchellNetravali(...)]
// (see below))
return (12.0f - (9.0f * B) - (6.0f * C)) * filtValCub +
(-18.0f + (12.0f * B) + (6.0f * C)) * filtValSqr +
(6.0f - (2.0f * B));
}
}
// Wrapper function applying Mitchell-Netravali to the given sampling point within the
// given sampling radius
float MitchellNetravali(float2 sampleXY,
float sampleRad)
{
float2 filtAxes = abs((sampleXY / sampleRad) * 2.0f);
return (MitchellNetravaliPerAxis(filtAxes.x) *
MitchellNetravaliPerAxis(filtAxes.y)) / 36.0f; // Dividing by 6 in each filter is equivalent to dividing the product of the filters by thirty-six (since 36 == 6^2)
}
Temporal integrator:
float3 FrameSmoothing(float3 rgb,
float filtCoeff,
uint linPixID)
{
// Cache a local copy of the given pixel's smoothing history
PixHistory pixHistory = aaBuffer[linPixID];
// Update the given pixel's sample count
uint sampleCount = pixHistory.sampleCount.x + 1;
uint sampleNdx = pixHistory.sampleCount.x % NUM_AA_SAMPLES;
// Update accumulated filter values
float4 currChanFilt = filtCoeff.xxxx;
float4 prevChanFilt = pixHistory.filtSet[sampleNdx];
float4 accumFilt = (pixHistory.filtAccum - prevChanFilt) + filtCoeff.xxxx;
// Filter the pixel, update accumulated pixel values
float4 currChanSample = float4(rgb, 1.0f) * filtCoeff;
float4 prevChanSample = pixHistory.sampleSet[sampleNdx];
float4 accumRGB = (pixHistory.sampleAccum - prevChanSample) + currChanSample;
// Pass the updated pixel history back into the AA buffer
// (also update the pixel's sample-set to include the current sample)
aaBuffer[linPixID].sampleAccum = accumRGB;
aaBuffer[linPixID].filtAccum = accumFilt;
aaBuffer[linPixID].sampleSet[sampleNdx] = currChanSample;
aaBuffer[linPixID].filtSet[sampleNdx] = filtCoeff;
aaBuffer[linPixID].sampleCount = sampleCount.xxxx;
// Return the updated sample
// Some filter functions can push average values below zero; counter this clamping out negative channels
// with [max(...)]
return max(accumRGB.rgb / accumFilt.xxx, 0.0f.xxx);
}