BoltBait Posted August 6, 2015 Share Posted August 6, 2015 (edited) 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: 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: (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: 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: 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: If that's too much, how about an up vote? Edited January 19, 2018 by BoltBait Updated tutorial for CodeLab v3.1 1 6 Quote Download: BoltBait's Plugin Pack | CodeLab | and a Free Computer Dominos Game Link to comment Share on other sites More sharing options...
Ego Eram Reputo Posted August 6, 2015 Share Posted August 6, 2015 Excellent tutorial! :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. Quote ebook: Mastering Paint.NET | resources: Plugin Index | Stereogram Tut | proud supporter of Codelab plugins: EER's Plugin Pack | Planetoid | StickMan | WhichSymbol+ | Dr Scott's Markup Renderer | CSV Filetype | dwarf horde plugins: Plugin Browser | ShapeMaker Link to comment Share on other sites More sharing options...
MJW Posted August 6, 2015 Share Posted August 6, 2015 (edited) 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 August 6, 2015 by MJW Quote Link to comment Share on other sites More sharing options...
BoltBait Posted August 6, 2015 Author Share Posted August 6, 2015 Excellent tutorial! :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. 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. Quote Download: BoltBait's Plugin Pack | CodeLab | and a Free Computer Dominos Game Link to comment Share on other sites More sharing options...
MJW Posted August 6, 2015 Share Posted August 6, 2015 (edited) 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 August 6, 2015 by MJW Quote Link to comment Share on other sites More sharing options...
Red ochre Posted August 6, 2015 Share Posted August 6, 2015 Useful and interesting tutorial and comments. 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. Quote Red ochre Plugin pack.............. Diabolical Drawings ................Real Paintings Link to comment Share on other sites More sharing options...
BoltBait Posted August 6, 2015 Author Share Posted August 6, 2015 Useful and interesting tutorial and comments. 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. Quote Download: BoltBait's Plugin Pack | CodeLab | and a Free Computer Dominos Game Link to comment Share on other sites More sharing options...
toe_head2001 Posted January 16, 2018 Share Posted January 16, 2018 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); } 1 Quote (September 25th, 2023) Sorry about any broken images in my posts. I am aware of the issue. My Gallery | My Plugin Pack Layman's Guide to CodeLab Link to comment Share on other sites More sharing options...
BoltBait Posted January 16, 2018 Author Share Posted January 16, 2018 That's some good info, @toe_head2001. CodeLab should be writing that code for you. I'll add it to the list of features for the next release. Quote Download: BoltBait's Plugin Pack | CodeLab | and a Free Computer Dominos Game Link to comment Share on other sites More sharing options...
MJW Posted January 16, 2018 Share Posted January 16, 2018 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. 1 Quote Link to comment Share on other sites More sharing options...
toe_head2001 Posted January 17, 2018 Share Posted January 17, 2018 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. 1 Quote (September 25th, 2023) Sorry about any broken images in my posts. I am aware of the issue. My Gallery | My Plugin Pack Layman's Guide to CodeLab Link to comment Share on other sites More sharing options...
MJW Posted January 17, 2018 Share Posted January 17, 2018 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.) Quote Link to comment Share on other sites More sharing options...
toe_head2001 Posted January 17, 2018 Share Posted January 17, 2018 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. Quote (September 25th, 2023) Sorry about any broken images in my posts. I am aware of the issue. My Gallery | My Plugin Pack Layman's Guide to CodeLab Link to comment Share on other sites More sharing options...
MJW Posted January 17, 2018 Share Posted January 17, 2018 That makes sense. I'll try to add dispose code to my plugins that allocate surfaces. Quote Link to comment Share on other sites More sharing options...
BoltBait Posted January 17, 2018 Author Share Posted January 17, 2018 With the new version 3.1 of CodeLab out, I really need to rewrite this tutorial. It is MUCH easier now! Quote Download: BoltBait's Plugin Pack | CodeLab | and a Free Computer Dominos Game Link to comment Share on other sites More sharing options...
BoltBait Posted January 18, 2018 Author Share Posted January 18, 2018 8 hours ago, BoltBait said: With the new version 3.1 of CodeLab out, I really need to rewrite this tutorial. It is MUCH easier now! ...and, it's done. 1 1 Quote Download: BoltBait's Plugin Pack | CodeLab | and a Free Computer Dominos Game Link to comment Share on other sites More sharing options...
lynxster4 Posted September 9, 2018 Share Posted September 9, 2018 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... 1 Quote My Art Gallery | My Shape Packs | ShapeMaker Mini Tut | Air Bubble Stained Glass Chrome Text with Reflections | Porcelain Text w/ Variegated Coloring | Realistic Knit PatternOpalescent Stained Glass | Frosted Snowman Cookie | Leather Texture | Plastic Text | Silk Embroidery Visit my Personal Website "Never, ever lose your sense of humor - you'll live longer" Link to comment Share on other sites More sharing options...
BLUEnLIVE Posted September 25, 2019 Share Posted September 25, 2019 (edited) 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 September 25, 2019 by BLUEnLIVE Quote Link to comment Share on other sites More sharing options...
BoltBait Posted September 25, 2019 Author Share Posted September 25, 2019 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! Quote Download: BoltBait's Plugin Pack | CodeLab | and a Free Computer Dominos Game Link to comment Share on other sites More sharing options...
BLUEnLIVE Posted September 25, 2019 Share Posted September 25, 2019 Thank you for kind reply. I'll try it! Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.