3

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);
}

Mitchell-Netravali, unjittered: Mitchell-Netravali, no jitter

Mitchell-Netravali, jittered: Mitchell-Netravali, with jitter

Unjittered/jittered box-filtered images for comparison: Box filter, no jitter Box filter, jittered

Paul Ferris
  • 447
  • 2
  • 11

1 Answers1

3

Mitchell–Netravali has negative lobes, which are generally not recommended for small sample counts from what I understand; you tend to end up with both the positive and negative areas undersampled. Also, negative lobes do produce a sharpening or ringing effect that looks like what you're seeing.

I'd try a nonnegative filter, such as a cut-off Gaussian or a Blackman–Harris window.

Nathan Reed
  • 25,002
  • 2
  • 68
  • 107
  • Thanks! I'm guessing Blackman-Harris is blurry (no negative lobes), but not as much as the Gaussian? – Paul Ferris Jan 23 '18 at 08:27
  • 1
    @PaulFerris I think that's right, although the two are quite similar in shape, so there's probably not a lot of difference. The filter width can be tuned to trade-off between blurring and aliasing, as well. – Nathan Reed Jan 23 '18 at 19:24