Jump to content

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


BoltBait

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 build an effect that requires an additional surface.

 

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.

 

 

Generating Code

 

The first thing you'll need is an icon. Either design one in paint.net or use this one: ObjectShadow.png (You can save that to your system as a .png file.)

 

Let's get started in CodeLab.  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, Normal Blend Mode, and a WRK surface, so select those options:

 

FileNewOptions.png

 

 

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

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

// Working surface
Surface wrk = null;

// Setup for calling the Gaussian Blur effect
GaussianBlurEffect blurEffect = new GaussianBlurEffect();
PropertyCollection blurProps;

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


void PreRender(Surface dst, Surface src)
{
    if (wrk == null)
    {
        wrk = new Surface(src.Size);
    }

    blurProps = blurEffect.CreatePropertyCollection();
    PropertyBasedEffectConfigToken BlurParameters = new PropertyBasedEffectConfigToken(blurProps);
    BlurParameters.SetPropertyValue(GaussianBlurEffect.PropertyNames.Radius, Amount1);
    blurEffect.SetRenderInfo(BlurParameters, new RenderArgs(wrk), new RenderArgs(src));
}

// Here is the main render loop function
void Render(Surface dst, Surface src, Rectangle rect)
{
    // Call the Gaussian Blur function
    blurEffect.Render(new Rectangle[1] {rect},0,1);

    // 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++)
        {
            ColorBgra CurrentPixel = wrk[x,y];

            // TODO: Add additional pixel processing code here

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


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


protected override void OnDispose(bool disposing)
{
    // Release any surfaces or effects you've created.
    if (wrk != null)
    {
        wrk.Dispose();
        wrk = null;
    }

    if (blurEffect != null)
    {
        blurEffect.Dispose();
        blurEffect = null;
    }

    base.OnDispose(disposing);
}

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:

 

UIDesigner.png

 

We'll also need a Double Slider for Shadow Strength (Style: White-Black, Min: 0, Default: 0.5, Max: 1) and a Colorwheel (Default: Black) for the shadow color.  Go ahead and add those controls as shown above.

 

 

Click OK to update your script with the new UI control.  Your UICode region should now look like this:

#region UICode
IntSliderControl Amount1 = 10; // [0,100] Radius
PanSliderControl Amount2 = Pair.Create(0.000,0.000); // Shadow Offset
DoubleSliderControl Amount3 = 0.5; // [0,1,4] Shadow Strength
ColorWheelControl Amount4 = ColorBgra.FromBgr(0,0,0); // [Black] Shadow Color
#endregion

The Shadow Radius of 10 (Amount1) is going to be much too big for most applications.  Let's change it to 4.

 

Finally, fill in all of the comment lines at the top of the script. The top of 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
IntSliderControl Amount1 = 4; // [0,100] Radius
PanSliderControl Amount2 = Pair.Create(0.000,0.000); // Shadow Offset
DoubleSliderControl Amount3 = 0.5; // [0,1,4] Shadow Strength
ColorWheelControl Amount4 = ColorBgra.FromBgr(0,0,0); // [Black] Shadow Color
#endregion

Let's take a moment to save our work.

 

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

 

 

Adjusting Double Vector Defaults

 

In the CodeLab UI Builder screen, we can't adjust the defaults of the Double Vector control.  Let's go ahead and edit that right in the CodeLab editor.  Change the following Amount2 line from:

PanSliderControl Amount2 = Pair.Create(0.000,0.000); // Shadow Offset

to:

PanSliderControl Amount2 = Pair.Create(0.02,0.02); // Shadow Offset

This will eventually make our default shadow 2 pixels to the right and 2 pixels down from our objects by multiplying those values by 100.

 

 

Move the Blur from Render to PreRender

 

Locate your Render function and cut the following 2 lines from the top of that function:

// Call the Gaussian Blur function
blurEffect.Render(new Rectangle[1] {rect},0,1);

Now, paste those lines into the bottom of your PreRender function.  Your PreRender code should now look like this:

void PreRender(Surface dst, Surface src)
{
    if (wrk == null)
    {
        wrk = new Surface(src.Size);
    }

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

That won't compile the way it is, so let's fix that last line.  Change it to say:

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

This tells Gaussian Blur to render the entire work surface instead of just one rectangle of it.

 

Typically, we will want to call Gaussian Blur in the Render function because then the work will be split up between all of your processors (multi-processor).  But, in the case of our Object Shadow plugin, we need to PreRender the entire object to the WRK canvas before the main Render function starts.

 

Be careful what you put in the PreRender function as everything in there runs in a single thread, so it will be much slower than stuff running in the Render function.

 

 

Final Render

 

Now it is time to finish 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:

// Here is the main render loop function
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++)
        {
            ColorBgra CurrentPixel = wrk[x,y];

            // TODO: Add additional pixel processing code here

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


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

 

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

ColorBgra CurrentPixel = wrk[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, right above the line that says "ColorBgra CurrentPixel = wrk[x,y];".

 

Next, we will be changing the following line:

ColorBgra CurrentPixel = wrk[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.  That's the "Shift right/down" portion of item #2 of our plan.

 

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:

// apply color to our shadow
CurrentPixel.R = Amount4.R;
CurrentPixel.G = Amount4.G;
CurrentPixel.B = Amount4.B;

That implements the "color it black" part of item #2 of our plan.

 

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 right after the last lines we just inserted:

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

By default, this lightens the shadow by half.  (If Amount3 is equal to 0.5, multiplying the CurrentPixel alpha by Amount3 will lighten it by half.)

 

Only one thing from our original plan left to do, #3. Replace the following line of automatically generated 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:

// Here is the main render loop function
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);

            // add color to our shadow, default black
            CurrentPixel.R = Amount4.R;
            CurrentPixel.G = Amount4.G;
            CurrentPixel.B = Amount4.B;
            CurrentPixel.A = (byte)(CurrentPixel.A * Amount3);

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

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

Let's take another moment to save our work.

 

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

 

 

Building a DLL

 

Now that we've completed the code, use CodeLab's Build DLL function (Ctrl+B) to create a real plugin for paint.net.  Be sure to select that icon you saved to your system so that it will appear in paint.net's menu with an icon.

 

Once built, check your desktop for two files: DropShadowDemo.dll and Install_DropShadowDemo.bat

 

You can run the install batch file located on your desktop to install your new DLL into paint.net.  Restart paint.net to see your new effect in the Effect menu.

 

 

Final Thoughts

 

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

 

If you enjoyed this tutorial and would like to buy me a beer, click this button:

 

btn_donate_SM.gif

 

If that's too much, how about an up vote?

 

Edited by BoltBait
Updated tutorial for CodeLab v3.1
  • Like 1
  • Upvote 6
Link to comment
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.

Link to comment
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
Link to comment
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.

Link to comment
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
Link to comment
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. ;)

 

Red ochre Plugin pack.............. Diabolical Drawings ................Real Paintings

 

PdnForumSig2.jpg

Link to comment
Share on other sites

  • 1 year later...

Today I realized none of my effects dispose their extra surfaces, and they just stay in memory... adding up every time the effect is run. :( This becomes very apparent when working on huge images.

The "child" effects should also be disposed.

 

This solves that issue.

protected override void OnDispose(bool disposing)
{
    wrk?.Dispose();
    blurEffect?.Dispose();
    base.OnDispose(disposing);
}

The Null-Conditional operator doesn't work in CodeLab, so you'll have to do it the old fashion way there.

protected override void OnDispose(bool disposing)
{
    if (wrk != null)
    	wrk.Dispose();
  
    if (blurEffect != null)
    	blurEffect.Dispose();
  
    base.OnDispose(disposing);
}
  • Like 1

(September 25th, 2023)  Sorry about any broken images in my posts. I am aware of the issue.

bp-sig.png
My Gallery  |  My Plugin Pack

Layman's Guide to CodeLab

Link to comment
Share on other sites

I've never disposed of them either. I've always assumed (probably incorrectly) that when the effect is completed and the effect class instance is disposed of, any surfaces created by the effect will be garbage collected.

  • Like 1
Link to comment
Share on other sites

Yeah, IDisposable objects will eventually get garbage collected. In the mean time, your heap could grow extremely large.

 

I did some tests with my Blur Fill plugin on a ~26 megapixel image, and looked at the memory usage of the paint.net process.

Before adding the explicit disposal, the memory kept growing every time I ran the plugin. After about 5 runs, it was over 1,200 MB.

After adding the explicit disposal, it immediately went back down to approximately 350 MB after each run of the plugin.

  • Upvote 1

(September 25th, 2023)  Sorry about any broken images in my posts. I am aware of the issue.

bp-sig.png
My Gallery  |  My Plugin Pack

Layman's Guide to CodeLab

Link to comment
Share on other sites

This may be a foolish question, but does disposing of surfaces free up memory just because that's just how it happens to work, rather than being how it's guaranteed to work? I've always been under the impression that disposing is designed to free system resources held by a class, and doesn't necessarily initiate any garbage collection. (By "system resources," I mean things like files, not memory.)

Link to comment
Share on other sites

Disposing releases the memory of the 'unmanaged code' associated with an IDisposable object.

I don't know the full details, but there are different levels of garbage collection in .NET, and the "regular" level that runs most often doesn't take care if that.

 

If an object is not an IDisposable, then it doesn't have 'unmanaged code' associated with it, and thus doesn't need to be disposed. Normal garbage collection will take care if it.

(September 25th, 2023)  Sorry about any broken images in my posts. I am aware of the issue.

bp-sig.png
My Gallery  |  My Plugin Pack

Layman's Guide to CodeLab

Link to comment
Share on other sites

  • 7 months later...

Thank you @BoltBait for an enlightening series of coding lessons.

 

I especially liked this one because you explained some of the math.

 

I actually like your Drop Shadow plugin better than KVM's.  The shadow is softer and the text actually looks like it's floating.

 

I bought you a beer cause I'm out of rep points!  ?  Thanks, again...

  • Upvote 1
Link to comment
Share on other sites

  • 1 year later...

Thank you @BoltBait for your great job.

 

I'm developing a simple plugin with CodeLab.

I have three questions to ask you.
 

1. It seems that user interface has only 2 buttons(Ok, Cancel).

Can I add more button?

 

2. Can OnRender() method notice that it's final render(after user clicks Ok button) or preview render(when UI is on)?

I want to make following style plugin.

- When UI is on, it runs quick and dirty style render.

- And, when user clicks Ok button, it runs high quality render.

 

But, I don't know how to detect current state in OnRender().

 

3. Can I draw line on Surface object?

When this is declared....

Surface dst;

 

I know how to put a pixel.

dst[x,y] = CurrentPixel;

 

But, I don't know how to draw a line.

Should I make a DrawLine() method?

 

 

 

Thank you for reading.

It's really a great job!

 

Edited by BLUEnLIVE
Link to comment
Share on other sites

On 9/24/2019 at 6:37 PM, BLUEnLIVE said:

1. It seems that user interface has only 2 buttons(Ok, Cancel).

Can I add more button?

 

Yes. It is easy to add more buttons.

 

In CodeLab, press Ctrl+I for Interface Designer...

 

Or, you can read this tutorial: https://boltbait.com/pdn/CodeLab/help/tutorial2.php

 

On 9/24/2019 at 6:37 PM, BLUEnLIVE said:

2. Can OnRender() method notice that it's final render(after user clicks Ok button) or preview render(when UI is on)?

I want to make following style plugin.

- When UI is on, it runs quick and dirty style render.

- And, when user clicks Ok button, it runs high quality render.

 

But, I don't know how to detect current state in OnRender().

 

That is not possible.

 

This is a feature request I've asked Rick to implement, but he hasn't gotten to it yet.

 

What we (as plugin authors) generally do is have a check box for the mode (see my Level Horizon plugin) or have a Quality slider to select your desired quality.

 

On 9/24/2019 at 6:37 PM, BLUEnLIVE said:

3. Can I draw line on Surface object?

 

Yes, no problem.  See this tutorial: https://boltbait.com/pdn/CodeLab/help/tutorial4.php

 

Hope this helps!

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...