Sign in to follow this  
MJW

Multisample antialiasing

Recommended Posts

Plugins that distort an image can often benefit from multisample antialiasing. The idea is to divide each pixel into multiple subpixels, then average the subpixels. It's not a perfect method. It tends to slightly blur the image, and it slows rendering, since it requires computing many times the number of pixel values. Nevertheless, it often substantially improves the image by reducing ugly jaggy artifacts. Plugins that most likely benefit from mutisampling are those that use the GetBilinearSample calls.

 

Adding multisampling is often quite easy. I wrote code to do so, which I've used in quite a few plugins. Therefore, it's fairly well tested.  I will explain how it can be added to other plugins.

 

Let's start with a simple plugin I call "Drain." It's a simplified version of the Twist effect.

 

Here is the original version:

 

Spoiler

// Name: DrainNoAA
// Submenu: Distort
// Author: MJW
// Title: DrainNoAA
// Version: 1.0
// Desc: Simplified version of Twist to demonstrate antialiasing
// Keywords: antialias
// URL:
// Help:
#region UICode
DoubleSliderControl Amount1 = 0; // [-1,1] Twist Amount
PanSliderControl Amount2 = Pair.Create(0.00,0.000); // Location
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
    float xScale = 0.5f * src.Width, yScale = 0.5f * src.Height;
    float cX = xScale * ((float)Amount2.First + 1.0f);
    float cY = yScale * ((float)Amount2.Second + 1.0f);
    double distScale = 0.03 * Amount1;

    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            float dX = x - cX;
            float dY = y - cY;
            double angle = distScale * Math.Sqrt(dX * dX + dY * dY);
            float sin = (float)Math.Sin(angle);
            float cos = (float)Math.Cos(angle);
            float tX = cos * dX - sin * dY + cX;
            float tY = sin * dX + cos * dY + cY;
            dst[x, y] = src.GetBilinearSampleWrapped(tX, tY);
        }
    }
}

 

 

The first step is to move the pixel computation to a separate method. The X,Y coordinates of the destination pixel must be passed to the method as floats. The method must transform the destination coordinates, fetch the corresponding pixel (usually with a call to a GetBilinearSample routine) , and return it as a ColorBgra.

 

Because it's a separate method, any parameters it needs must either be passed as arguments or through "global" -- i.e., class-level -- variables. Because multiple rendering routines may be running at the same time, values that change from one call to the next (within the same rendering pass) must be passed as arguments. ( I've used multisampling in a variety of plugins, and never needed to pass extra arguments.)

 

Here is the Drain plugin modified to move the transformation to a separate method. It does exactly the same thing as the previous version.

 

Spoiler

// Name: DrainPreAA
// Submenu: Distort
// Author: MJW
// Title: DrainPreAA
// Version: 1.0
// Desc: Simplified version of Twist to demonstrate antialiasing
// Keywords: antialias
// URL:
// Help:
#region UICode
DoubleSliderControl Amount1 = 0; // [-1,1] Twist Amount
PanSliderControl Amount2 = Pair.Create(0.00,0.000); // Location
#endregion

Surface Src;
float cX, cY;
double distScale;

void Render(Surface dst, Surface src, Rectangle rect)
{
    Src = src;
    float xScale = 0.5f * src.Width, yScale = 0.5f * src.Height;
    cX = xScale * ((float)Amount2.First + 1.0f);
    cY = yScale * ((float)Amount2.Second + 1.0f);
    distScale = 0.03 * Amount1;

    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            dst[x, y] = Transform(x, y);
        }
    }
}

// The transformation is put in a method that accepts float coordinates and
// returns the pixel as a ColorBgra.
// Parameters that never change can be passed as "global" (class-level) variables.
// If these parameters are calculated in the Render routine, they must never be
// assigned anyting except their final values.
ColorBgra Transform(float x, float y)
{
    float dX = x - cX;
    float dY = y - cY;
    double angle = distScale * Math.Sqrt(dX * dX + dY * dY);
    float sin = (float)Math.Sin(angle);
    float cos = (float)Math.Cos(angle);
    float tX = cos * dX - sin * dY + cX;
    float tY = sin * dX + cos * dY + cY;
    return Src.GetBilinearSampleWrapped(tX, tY);
}

 

 

Finally, I'll add the antialias code. The process of adding the code is mostly mechanical. First, controls need to be added to allow the user to specify the number of subsamples per pixel. Second, the loop must be modified to call the antialias version of the transform code when antialiasing is enabled. (The antialias code works with one subsample per pixel. I call the single-sample version when antialiasing is disabled for efficiency) Finally, the two antialias routines must be added. These routines can be more-or-less copied in. SetupForSubpixels never changes. The only thing that might change in the antialias transform method is the name of the transform routine, and the addition of extra arguments, if arguments besides the coordinates must be passed to the transformation routine.

 

Here is the antialias verison of Drain. Two methods are added: TransformAA and SetupForSubpixles.

 

Spoiler

// Name: DrainAA
// Submenu: Distort
// Author: MJW
// Title: DrainAA
// Version: 1.0
// Desc: Simplified version of Twist to demonstrate antialiasing
// Keywords: antialias
// URL:
// Help:
#region UICode
DoubleSliderControl Amount1 = 0; // [-1,1] Twist Amount
PanSliderControl Amount2 = Pair.Create(0.00,0.000); // Location
bool Amount3 = false; // Antialias
int Amount4 = 4; // [2, 6] Antialias Quality

#endregion

Surface Src;
float cX, cY;
double distScale;

void Render(Surface dst, Surface src, Rectangle rect)
{
    Src = src;
    
    // Setup anialiasing.
    bool antialias = Amount3;
    if (antialias)
        SetupForSubpixels(Amount4, Amount4);
       
    float xScale = 0.5f * src.Width, yScale = 0.5f * src.Height;
    cX = xScale * ((float)Amount2.First + 1.0f);
    cY = yScale * ((float)Amount2.Second + 1.0f);
    distScale = 0.03 * Amount1;

    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            dst[x, y] = antialias ? TransformAA(x, y) : Transform(x, y);
        }
    }
}

// The transformation is put into a method that accepts float coordinates and
// returns the pixel as a ColorBgra.
// Parameters that never change can be passed as "global" (class-level) variables.
// If these parameters are calculated in the Render routine, they must never be
// assigned anyting except their final values.
ColorBgra Transform(float x, float y)
{
    float dX = x - cX;
    float dY = y - cY;
    double angle = distScale * Math.Sqrt(dX * dX + dY * dY);
    float sin = (float)Math.Sin(angle);
    float cos = (float)Math.Cos(angle);
    float tX = cos * dX - sin * dY + cX;
    float tY = sin * dX + cos * dY + cY;
    return Src.GetBilinearSampleWrapped(tX, tY);
}

// ========== Antialias code ==========

// Antialias version of Transform.
// This code is essentially the same for all AA plugins.
// This version uses integer arithmetic to sum the subsamples.
// The code could be implemented using floats. If floats are used,
// the divison used to scale the colors can be compued by calculating
// the reciprocal, then multiplying each color.
ColorBgra TransformAA(int cx, int cy)
{
    ColorBgra pixel = ColorBgra.White;

    float bx = (float)cx - ssXStart;            
    float y = (float)cy - ssYStart;
   
    int b = 0, g = 0, r = 0, a = 0;
    for (int i = 0; i < ssYSamples; i++)
    {
        float x = bx;
        for (int j = 0; j < ssXSamples; j++)
        { 
            pixel = Transform(x, y);
            int alpha = pixel.A;
            if (alpha != 0)
            {
                b += alpha * pixel.B;
                g += alpha * pixel.G;
                r += alpha * pixel.R;
                a += alpha;
            }
            x += ssXStep;    
        } 
        y += ssYStep;
    }

    if (a == 0)
    {
        return ColorBgra.FromBgra(0, 0, 0, 0);
    }
    else
    {
        // Compute the (rounded) averages.
        int twiceA = a << 1;
        b = ((b << 1) + a) / twiceA;
        g = ((g << 1) + a) / twiceA;
        r = ((r << 1) + a) / twiceA;
        a = (twiceA + ssSamples) / ssTwiceSamples;
        return ColorBgra.FromBgra((byte)b, (byte)g, (byte)r, (byte)a); 
    }
}   

// Set up for subsampling.
// The number of X and Y subsamples can be different, thugh they usually won't be.
int ssXSamples, ssYSamples, ssSamples, ssTwiceSamples;
float ssXStart, ssYStart, ssXStep, ssYStep;
void SetupForSubpixels(int xSamples, int ySamples)
{
    ssXSamples = xSamples;
    ssYSamples = ySamples;
    ssSamples = xSamples * ySamples;
    ssTwiceSamples = ssSamples << 1;
    ssXStep = 1.0f / (float)xSamples;
    ssYStep = 1.0f / (float)ySamples;
    ssXStart = 0.5f * (1.0f - ssXStep);
    ssYStart = 0.5f * (1.0f - ssYStep);  
}

 


 

NOTES:

1) As noted in the code, I use integer arithmetic to sum the color components, which somewhat complicates the routines. In some cases it might be better to use floats or doubles to accumulate the colors. I believe the integer version is probably faster for most processors, but I've never run performance tests.

 

2) In my example, I use a checkbox to enable antialiasing, and a slider that starts a 2. Obviously the checkbox could be eliminated, and the slider made to start at 1. I generally use the checkbox enable because I like being able to easily switch AA on and off. Which method is best depends on the plugin and the programmer's taste.

 

3) In the TransformAA, before adding to the sums I test if alpha is zero with "if (alpha != 0)". That's unnecessary, and actually may not be such a good idea. It saves computation time if there are a lot of transparent pixels, but may make an unpredictable branch that's worse than the adds and multiplies.

 

4) If the sum of the alphas is zero, I return the BGRA color (0, 0, 0, 0). Perhaps it would make sense to return instead ColorBgra.Transparent, which is (255, 255, 255, 0). (I think it would be better if Transparent were (0, 0, 0, 0).)

 

5) I go to extra effort to produce rounded results. I think it's probably a good idea, but it may be an unnecessary complication. Without rounding, the computations are simply:

// Compute the averages
b /= a;
g /= a;
r /= a;
a /= ssSamples;

6) If you're writing your own multisampling AA routines, you might at least want to use my calculation of ssXStrart and ssYStart, which are the offsets to the pixel's first subsample. I think they're a little tricky to get right, and if they're wrong, the image will shift slightly when AA is enabled.

  • Upvote 3

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this