Sign in to follow this  
BoltBait

How to Write an Effect Plugin (Part 7 - Extra Surface)

Recommended Posts

Sometimes you come up with an idea for a plugin and it is just a little too complex for the normal limitations of the plugin system.

Normally, you're limited to two surfaces (the source surface and the destination surface), one built-in effect (like Gaussian Blur or Clouds, etc.), and an unlimited number of unary pixel ops and blend ops.

What if you need an additional built-in effect? Or, what if you need an additional surface to precompute something? In this tutorial, I thought I'd walk you through using CodeLab to start a complex effect and finish it off in Visual Studio.

The first thing you'll need to do is come up with an idea and design it out. In this tutorial, I'll walk you through designing and coding a "drop shadow" plugin.

The Design

Our plugin will start by using Gaussian blur our source canvas to a working canvas. Then, we will shift that result over and down a couple of pixels to our destination canvas. Finally, we will combine the source canvas with the destination canvas to retain our original object that is casting a shadow:

ShadowPlan.png

If we didn't need to shift the shadow down and right, we would be able to do this without an extra surface. But, there is no practical way to blur and shift at the same time. So, let's go ahead and use an extra surface.

 

Gathering Code

The first thing you'll need is an icon. Either design one in paint.net or use this one: ObjectShadow.png

This effect is going to be too complex for a CodeLab script. But, let's start in CodeLab so that we can get it to write a bunch of code for us and then we'll move that project into Visual Studio to finalize our effect.

Run CodeLab and use the File > New menu. This will bring up the New Source (Template) screen.

We have already decided based on our design that we'll need Gaussian Blur and Normal Blend Mode, so select those options:

ShadowTemplate.png

Click the "Generate Code" button. Your script should look like this:

// Name:
// Submenu:
// Author:
// Title:
// Desc:
// Keywords:
// URL:
// Help:
#region UICode
int Amount1=10; // [0,100] Radius
#endregion

// Setup for using Normal blend op
private BinaryPixelOp normalOp = LayerBlendModeUtil.CreateCompositionOp(LayerBlendMode.Normal);

// Here is the main render loop function
void Render(Surface dst, Surface src, Rectangle rect)
{
    // Setup for calling the Gaussian Blur effect
    GaussianBlurEffect blurEffect = new GaussianBlurEffect();
    PropertyCollection blurProps = blurEffect.CreatePropertyCollection();
    PropertyBasedEffectConfigToken BlurParameters = new PropertyBasedEffectConfigToken(blurProps);
    BlurParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
    blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(dst), new RenderArgs(src));
    // Call the Gaussian Blur function
    blurEffect.Render(new Rectangle[1] {rect},0,1);

    // Now in the main render loop, the dst canvas has a blurred version of the src canvas
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            ColorBgra CurrentPixel = dst[x,y];

            // TODO: Add additional pixel processing code here

            CurrentPixel = normalOp.Apply(src[x,y], CurrentPixel);


            dst[x,y] = CurrentPixel;
        }
    }
}

Our script is nowhere near working yet, so don't worry about that yet. We're just gathering code that we'll need to write our effect.

OK, now we have an effect with a slider for blur radius. Next we'll need to add a couple of sliders to handle the offset of the shadow. Use the File > User Interface Designer and add a double vector control:

ShadowUI.png

Click OK to update your script with the new UI control.

We're also going to need a working surface, so let's add the following code below the UI block:

// Working surface
Surface wrk = null;

Finally, fill in all of the comment lines at the top of the script. Your script should look something like this now:

// Name: Drop Shadow Demo
// Submenu: Object
// Author: BoltBait
// Title: Drop Shadow Demo
// Desc: Drop Shadow
// Keywords: drop|shadow
// URL: http://www.BoltBait.com/pdn
// Help:
#region UICode
int Amount1 = 10; // [0,100] Radius
Pair<double, double> Amount2 = Pair.Create( 0.0 , 0.0 ); // Shadow Offset
#endregion

// Working surface
Surface wrk = null;

// Setup for using Normal blend op
private BinaryPixelOp normalOp = LayerBlendModeUtil.CreateCompositionOp(LayerBlendMode.Normal);

// Here is the main render loop function
void Render(Surface dst, Surface src, Rectangle rect)
{
    // Setup for calling the Gaussian Blur effect
    GaussianBlurEffect blurEffect = new GaussianBlurEffect();
    PropertyCollection blurProps = blurEffect.CreatePropertyCollection();
    PropertyBasedEffectConfigToken BlurParameters = new PropertyBasedEffectConfigToken(blurProps);
    BlurParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
    blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(dst), new RenderArgs(src));
    // Call the Gaussian Blur function
    blurEffect.Render(new Rectangle[1] {rect},0,1);

    // Now in the main render loop, the dst canvas has a blurred version of the src canvas
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            ColorBgra CurrentPixel = dst[x,y];

            // TODO: Add additional pixel processing code here

            CurrentPixel = normalOp.Apply(src[x,y], CurrentPixel);


            dst[x,y] = CurrentPixel;
        }
    }
}

Use File > Save... command to save your script as "DropShadowDemo.cs"

 

Moving to Visual Studio

Now that you have everything you need in CodeLab, move that code to a Visual Studio project by following this tutorial:

http://boltbait.com/pdn/CodeLab/help/tutorial5.asp

Once you have it building in Visual Studio, let's continue...

 

Adjusting Defaults

In Visual Studio, look for the function called OnCreatePropertyCollection(). It should look like this:

protected override PropertyCollection OnCreatePropertyCollection()
{
    List<Property> props = new List<Property>();

    props.Add(new Int32Property(PropertyNames.Amount1, 10, 0, 100));
    props.Add(new DoubleVectorProperty(PropertyNames.Amount2, Pair.Create(0.0, 0.0), Pair.Create(-1.0, -1.0), Pair.Create(+1.0, +1.0)));

    return new PropertyCollection(props);
}

Let's change the Amount1 default from 10 to 4. That will make our drop shadow smaller. And, let's change the Amount2 default from "Pair.Create(0.0, 0.0)" to "Pair.Create(0.02, 0.02)". That will offset our drop shadow 2 pixels to the right and 2 pixels down.

Your code should now look like this:

protected override PropertyCollection OnCreatePropertyCollection()
{
    List<Property> props = new List<Property>();

    props.Add(new Int32Property(PropertyNames.Amount1, 4, 0, 100));
    props.Add(new DoubleVectorProperty(PropertyNames.Amount2, Pair.Create(0.02, 0.02), Pair.Create(-1.0, -1.0), Pair.Create(+1.0, +1.0)));

    return new PropertyCollection(props);
}

 

Creating Working Surface

Next, look for the function titled OnSetRenderInfo. It currently looks like this:

protected override void OnSetRenderInfo(PropertyBasedEffectConfigToken newToken, RenderArgs dstArgs, RenderArgs srcArgs)
{
    Amount1 = newToken.GetProperty<Int32Property>(PropertyNames.Amount1).Value;
    Amount2 = newToken.GetProperty<DoubleVectorProperty>(PropertyNames.Amount2).Value;

    base.OnSetRenderInfo(newToken, dstArgs, srcArgs);
}

Any time the user changes one of the UI controls, this function runs and updates the Amount1 and Amount2 variables from the screen then the Render function is called to update the canvas. This gives us two places to put code that will run any time the UI changes. OnSetRenderInfo is run first in a single thread and Render is called next in multiple threads. Therefore, when you have a choice, you should put the faster code in OnSetRenderInfo and the slower code in Render.

It is in this function that we will be creating and working with our work surface as this code runs before our render function. This guarantees that our working surface will be ready when the render function runs.

We will use the following code to create the working surface if it doesn't already exist:

if (wrk == null)
{
    wrk = new Surface(srcArgs.Size);
}

This will create the wrk surface the same size as the src surface.

Place this code just before the "base.OnSetRenderInfo" call. Your code should now look like this:

protected override void OnSetRenderInfo(PropertyBasedEffectConfigToken newToken, RenderArgs dstArgs, RenderArgs srcArgs)
{
    Amount1 = newToken.GetProperty<Int32Property>(PropertyNames.Amount1).Value;
    Amount2 = newToken.GetProperty<DoubleVectorProperty>(PropertyNames.Amount2).Value;

    if (wrk == null)
    {
        wrk = new Surface(srcArgs.Size);
    }

    base.OnSetRenderInfo(newToken, dstArgs, srcArgs);
}

Now that we've created the work surface, let's populate that surface with the results of the Gaussian Blur function. Go down into our render function and CUT the following code out:

// Setup for calling the Gaussian Blur effect
GaussianBlurEffect blurEffect = new GaussianBlurEffect();
PropertyCollection blurProps = blurEffect.CreatePropertyCollection();
PropertyBasedEffectConfigToken BlurParameters = new PropertyBasedEffectConfigToken(blurProps);
BlurParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(dst), new RenderArgs(src));
// Call the Gaussian Blur function
blurEffect.Render(new Rectangle[1] { rect }, 0, 1);

Paste it in to our OnSetRenderInfo function just above the "base.OnSetRenderInfo" call. Your code should now look like this:

protected override void OnSetRenderInfo(PropertyBasedEffectConfigToken newToken, RenderArgs dstArgs, RenderArgs srcArgs)
{
    Amount1 = newToken.GetProperty<Int32Property>(PropertyNames.Amount1).Value;
    Amount2 = newToken.GetProperty<DoubleVectorProperty>(PropertyNames.Amount2).Value;

    if (wrk == null)
    {
        wrk = new Surface(srcArgs.Size);
    }

    // Setup for calling the Gaussian Blur effect
    GaussianBlurEffect blurEffect = new GaussianBlurEffect();
    PropertyCollection blurProps = blurEffect.CreatePropertyCollection();
    PropertyBasedEffectConfigToken BlurParameters = new PropertyBasedEffectConfigToken(blurProps);
    BlurParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
    blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(dst), new RenderArgs(src));
    // Call the Gaussian Blur function
    blurEffect.Render(new Rectangle[1] { rect }, 0, 1);

    base.OnSetRenderInfo(newToken, dstArgs, srcArgs);
}

You should see some red underlines in the code you just pasted in. We'll fix those now.

In the following line:

blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(dst), new RenderArgs(src));

Change it to:

blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(wrk), srcArgs);

This will cause it to render from the src surface to the wrk surface.

Finally, in this line:

blurEffect.Render(new Rectangle[1] { rect }, 0, 1);

Change it to:

blurEffect.Render(new Rectangle[1] { wrk.Bounds }, 0, 1);

This tells Gaussian Blur to render the entire work surface.

 

Final Render

Now it is time to fix our render function. As you remember from our plan, we've already handled #1 so we only have #2 and #3 to go.

Currently, our render function looks like this:

void Render(Surface dst, Surface src, Rectangle rect)
{

    // Now in the main render loop, the dst canvas has a blurred version of the src canvas
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            ColorBgra CurrentPixel = dst[x, y];

            // TODO: Add additional pixel processing code here

            CurrentPixel = normalOp.Apply(src[x, y], CurrentPixel);


            dst[x, y] = CurrentPixel;
        }
    }
}

That first comment is a little wrong. Actually, the wrk canvas has the blurred version of the src canvas. Go ahead and fix that comment.

We will keep the loops and only modify the code inside of the innermost loop. Starting with this code, let's implement #2 and #3:

ColorBgra CurrentPixel = dst[x, y];

// TODO: Add additional pixel processing code here

CurrentPixel = normalOp.Apply(src[x, y], CurrentPixel);


dst[x, y] = CurrentPixel;

The first thing we need to calculate is how far to shift the shadow. Using the double vector control is fairly easy. We only need to convert the first slider to the x-offset and the second slider to the y-offset:

// the shadow is on the work surface, now offset it
int offsetx = (int)(Amount2.First * 100);
int offsety = (int)(Amount2.Second * 100);

We are multiplying by 100 here to convert from 0.02 to 2. This will offset by 2 pixels. Insert this code at the top of our innermost loop.

Next, we will be changing the following line:

ColorBgra CurrentPixel = dst[x, y];

to:

ColorBgra CurrentPixel = wrk.GetBilinearSample(x - offsetx, y - offsety);

This will get our CurrentPixel off of the working canvas, offset by the amount specified by our double vector control.

If you recall, that pixel is blurred, but it may have some color. We need to change the shadow to black, so replace the following line:

// TODO: Add additional pixel processing code here

With:

// make shadow color black
CurrentPixel.R = 0;
CurrentPixel.G = 0;
CurrentPixel.B = 0;

That handles the R, G, and B portion of the shadow. But, we still need to calculate the Alpha. We could just leave it alone, but if we did, the shadow would be too dark. So, let's add the following line to lighten the shadow:

CurrentPixel.A = (byte)(CurrentPixel.A * 0.5);

This lightens the shadow by half.

Only one thing left, #3. Replace the following line of code:

CurrentPixel = normalOp.Apply(src[x, y], CurrentPixel);

with:

CurrentPixel = normalOp.Apply(CurrentPixel, src[x, y]);

This will mix the original surface with the shadow to retain the original object. The "normalOp" function mixes the pixel on the right with the pixel on the left as if the pixel on the right is on a higher layer than the pixel on the left. In this case, we want the original pixel to be placed on top of the shadow, so we needed to specify the CurrentPixel (our shadow) on the left and the "src[x,y]" pixel (our original object) on the right.

Your final render function should now look like this:

void Render(Surface dst, Surface src, Rectangle rect)
{
    // Now in the main render loop, the wrk canvas has a blurred version of the src canvas
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            // the shadow is on the work surface, now offset it
            int offsetx = (int)(Amount2.First * 100);
            int offsety = (int)(Amount2.Second * 100);

            ColorBgra CurrentPixel = wrk.GetBilinearSample(x - offsetx, y - offsety);

            // apply shadow color
            CurrentPixel.R = 0;
            CurrentPixel.G = 0;
            CurrentPixel.B = 0;

            CurrentPixel.A = (byte)(CurrentPixel.A * 0.5);

            CurrentPixel = normalOp.Apply(CurrentPixel, src[x, y]);

            dst[x, y] = CurrentPixel;
        }
    }
}

Build your .dll from within Visual Studio and try it out in paint.net!

 

Final Thoughts

Now that you have something working, you may want to add a few features, like a color wheel to select the shadow color and a slider to adjust the shadow strength.

Or, you could just load my plugin pack which has a full featured version of this plugin. ;)

I hope you learned something from this tutorial. Now, show me what you can do!

___________________

ShadowPlan.png

ShadowTemplate.png

ShadowUI.png

  • Upvote 7

Share this post


Link to post
Share on other sites

Excellent tutorial! :star: :star: :star:

One minor point - Where you say

 

We need to change the shadow to black, so replace the following line:

 

// TODO: Add additional pixel processing code here

With:

 

// make shadow color black

CurrentPixel.R = 0;

CurrentPixel.G = 0;

CurrentPixel.B = 0;

Why not use the destaturate pixelop or even the secondary color? It seems less versatile (to me) to hard code the color.

Share this post


Link to post
Share on other sites

This seems to answer a question I was wondering about recently: whether it's possible to use the built-in Gaussian blur function to blur outside the selection when writing to a user surface. I have an idea for a plugin that uses that.

 

I'm not sure I'd use GetBilinearSample. I don't see much advantage to using the slower GetBilinearSample when the indices are integers. I suppose if the indices might be out of bounds, it saves doing your own clamping.

 

I suggest all the code to be called from OnSetRenderInfo be collected in a function included (but never called) in the CodeLab version. Then once it's moved to VS, all that's needed is to add the function call to OnSetRenderInfo. That makes it easier to repeat the process if the controls need to be changed, and if, like me, you hate the idea of modifying the controls in VS.

 

My experience is that the easiest way to create VS projects from CodeLab is to create a single generic VS project, with the CodeLab code in a file with some name like EffectPlugin.cpp. To create a new project, copy and rename the generic project folder, then change the AssemblyName in VS to the desired plugin name. Paste the generated Codelab source as the EffectPlugin.cpp file. This seems to work correctly, though I always worry that there's something else I should do, since I don't know exactly where PDN gets various information about the plugin. The icon has to be handled specially by changing StaticIcon to:

 public static Bitmap StaticIcon { get { return EffectPlugin.Properties.Resources.Icon; } }

To the post-build event command line, I suggest adding: copy  /y   "$(TargetFileName)" "C:\Program Files\Paint.NET\Effects"

Edited by MJW

Share this post


Link to post
Share on other sites

Excellent tutorial! :star: :star: :star:

Thank you!

 

Why not use the destaturate pixelop or even the secondary color? It seems less versatile (to me) to hard code the color.

Desaturate wouldn't work at all. What if you typed white text on a blank canvas? Your code would blur that and desaturate it to... white... your shadow would be white on white text. Secondary color is almost as bad... it is usually white too. Primary color might be a good way to go. But, in this case, your Primary color is probably set to blue (since you just typed the text). Therefore, it would make a blue shadow with blue text. Not good. I think it is best to just hard code it to black.

Why hard code it? Well, I simplified the real code down as much as possible for the tutorial. As mentioned in the closing, adding a color wheel is an exercise left up to the reader. :D

 

I'm not sure I'd use GetBilinearSample. I don't see much advantage to using the slower GetBilinearSample when the indices are integers. I suppose if the indices might be out of bounds, it saves doing your own clamping.

That was exactly the reason I chose the function. It keeps the code simple for the demo.

 

My experience is that the easiest way to create VS projects from CodeLab is to create a single generic VS project, with the CodeLab code in a file with some name like EffectPlugin.cpp...

Just be careful you don't reuse the same namespace as another effect. That could lead to trouble.

Share this post


Link to post
Share on other sites

Is it really the namespace that matters? I thought the namespace was something C# uses to distinguish between different variables with the same names. I never change the namespace name, and I haven't yet encountered any problems. I do know that the AssemblyName has to be different or PDN gets confused.

Edited by MJW

Share this post


Link to post
Share on other sites

Useful and interesting tutorial and comments.   B)  Many thanks!

I normally use another surface to get around R.O.I 'striping' using the example provided by Null54 in the 'Furblur' thread (second post):
http://forums.getpaint.net/index.php?/topic/27013-furblur-roi-random-problems/
(Which is a set up in a different way).

I hope to work through the tutorial more fully later. ;)

Share this post


Link to post
Share on other sites

Useful and interesting tutorial and comments.   B)  Many thanks!

Thanks!

 

I hope to work through the tutorial more fully later. ;)

I just clarified a few areas of the tutorial so hopefully it makes more sense now.

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