<p>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.</p>

<p>I'd try a nonnegative filter, such as a cut-off Gaussian or a <a href="https://en.wikipedia.org/wiki/Window_function#Blackman%E2%80%93Harris_window" rel="nofollow noreferrer">Blackman–Harris window</a>.</p>

CC BY-SA 3.0
2018-01-23T08:27:28.580Z
Thanks! I'm guessing Blackman-Harris is blurry (no negative lobes), but not as much as the Gaussian?
Paul Ferris
CC BY-SA 3.0
2018-01-23T19:24:45.653Z
@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
CC BY-SA 3.0
2018-01-23T06:01:14.710Z
2018-01-23T06:01:14.710Z
Nathan Reed
<p>I recently implemented Mitchell-Netravali filtering in my path-tracer with 16x (4x4) temporal anti-aliasing. PBR's demonstration image shows it <em>should</em> 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).</p>

<p>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.</p>

<p>Function for $f(x, y)$:</p>

<pre><code>// Small functions returning the Mitchell-Netravali filter coefficient
// for a given 2D point
// Mitchell-Netravali interpolates sparse samples much more effectively
// than naive filtering (=&gt; 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 &gt; 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)
}
</code></pre>

<p>Temporal integrator:</p>

<pre><code>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);
}
</code></pre>

<p>Mitchell-Netravali, unjittered:
<a href="https://i.sstatic.net/mm0v9.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/mm0v9.png" alt="Mitchell-Netravali, no jitter"></a></p>

<p>Mitchell-Netravali, jittered:
<a href="https://i.sstatic.net/Uwut6.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/Uwut6.png" alt="Mitchell-Netravali, with jitter"></a></p>

<p>Unjittered/jittered box-filtered images for comparison:
<a href="https://i.sstatic.net/hVrYR.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/hVrYR.png" alt="Box filter, no jitter"></a>
<a href="https://i.sstatic.net/PK0cs.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/PK0cs.png" alt="Box filter, jittered"></a></p>

<antialiasing><hlsl><filtering>
Unexpected results from Mitchell-Netravali
Paul Ferris
# Unexpected results from Mitchell-Netravali

(Asked by: username_0 on 2018-01-22)

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](https://i.sstatic.net/mm0v9.png)](https://i.sstatic.net/mm0v9.png)

Mitchell-Netravali, jittered:
[![Mitchell-Netravali, with jitter](https://i.sstatic.net/Uwut6.png)](https://i.sstatic.net/Uwut6.png)

Unjittered/jittered box-filtered images for comparison:
[![Box filter, no jitter](https://i.sstatic.net/hVrYR.png)](https://i.sstatic.net/hVrYR.png)
[![Box filter, jittered](https://i.sstatic.net/PK0cs.png)](https://i.sstatic.net/PK0cs.png)

**Tags:** antialiasing, hlsl, filtering


---

## 1 answers

### Answer by username_1 on 2018-01-23. Score: 3 | Accepted answer

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](https://en.wikipedia.org/wiki/Window_function#Blackman%E2%80%93Harris_window).

> **Comments:**
> **username_0**: Thanks! I'm guessing Blackman-Harris is blurry (no negative lobes), but not as much as the Gaussian?  
> **username_1**: @username_0 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.  

---

