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


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.

  • $\begingroup$ Thanks! I'm guessing Blackman-Harris is blurry (no negative lobes), but not as much as the Gaussian? $\endgroup$ – Paul Ferris Jan 23 '18 at 8:27
  • 1
    $\begingroup$ @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. $\endgroup$ – Nathan Reed Jan 23 '18 at 19:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.