Jump to content

GPU Drawing Effect Questions


Go to solution Solved by Rick Brewster,

Recommended Posts

Quote

I'm happy to help with the conversion btw, just let me know what questions you've got!

I'm not having much success trying to convert a simple G.D.I.+ effect to use the new system.
I have followed @BoltBait 's tutorial (Many thanks) but still have many basic questions.
I will keep these questions in this one thread which may be of use for others learning the new system.
Hopefully they have relatively easy answers (well, the first three) and the new system will become clearer to me in time.
If there is already a reference of simple code snippets for common uses, please direct me to it.
Many thanks in advance.

 

1. How to get the Primary and Secondary colors?
Previously:

ColorBgra PC = (ColorBgra)EnvironmentParameters.PrimaryColor;

-----------------------
2. How to get pixel values from the src surface?
Previously:

ColorBgra srcColor = src.GetBilinearSampleWrapped(Xcoord, Ycoord);//or clamped etc.

Do I need to somehow lock and import the src surface?

Quote

You can use Environment.GetSourceBitmapBgra32(), which will give you an IEffectInputBitmap<ColorBgra32>. Then call Lock() to get the IBitmapLock<ColorBgra32>(). Then use AsRegionPtr() to get it as a RegionPtr<ColorBgra32> which you can just index into, e.g. region[x, y]. Then just compare region[x, y].A == 255 to determine if it's opaque.

???? What does that look like as code please? (straight over my head 😀).

------------------------
3. The correct new way to change the destination background colour or image?
Previously I would use something like:

dst.CopySurface(src, rect.Location, rect);// quickly copies Src to Dst
            if (TransparentBackground){ColorBgra klear = ColorBgra.Transparent; dst.Clear(rect, klear);}

-----------------------
4. Can GpuDrawingEffects be added to a BitmapEffect and does this need to done in a separate method?
... say you wanted to mess with the colour of the Src pixel by pixel then 'draw' something on top using Direct2D.
If so how is this done.
-----------------------
5. Single-threaded effects; Previously 'OnSetRenderInfo' could be used for single threaded calculations to set 'global' variables, which could then be used in 'Render'.
Is this approach still possible and if not how should this be done?
----------------------
6. From @BoltBait's tutorial I can draw ellipses and fill them with a SolidColorBrush but cannot fathom the values needed in the RadialGradientBrush.
Actually a more advanced tutorial on using paths and various types of brushes could be very useful!
The MS tutorials seem to assume a much deeper knowledge of C# and C++ than I have and I'm not sure how relevant they are within a Pdn effect.

Eg. https://learn.microsoft.com/en-us/windows/win32/direct2d/how-to-create-a-radial-gradient-brush

 

The quotes are Rick's from E.E.R.'s Box Outlining thread. ( I don't even know how to use quotes from different threads in this forum! 🙄)

  • Like 1

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

On 8/28/2024 at 2:35 PM, Red ochre said:

I have ... many basic questions.

 

First, there are 3 different types of effects you can write now. Overview Tutorial

 

The one most similar to the old Classic Effect is Bitmap Effect:

image.png

Bitmap Effect Tutorial 

 

Be sure to read this page as well.  It includes info about color spaces: Paint.NET Effect Rendering Overview

 

 

On 8/28/2024 at 2:35 PM, Red ochre said:

2. How to get pixel values from the src surface?

 

Once you follow the Bitmap Effect Tutorial you'll see how this is done.

 

 

 

On 8/28/2024 at 2:35 PM, Red ochre said:

3. The correct new way to change the destination background colour or image?

 

This is different depending on which of the 3 types of plugins you're writing.

 

Bitmap

 

// Get your source pixel
ColorBgra32 sourcePixel = sourceRegion[x,y];
if (TransparentBackground) sourcePixel.A = 0; // maintain RGB, but make fully transparent

 

GPU Drawing

 

Normally, a GPU Drawing effect would start by drawing the background:

 

protected override unsafe void OnDraw(IDeviceContext deviceContext)
{
    if (!TransparentBackground) deviceContext.DrawImage(Environment.SourceImage);
    :
    :

 

Just don't draw the background to start your effect and the resulting image will be transparent to start.

 

GPU Image

 

In order to maintain the background image, you'll need to mix what you've prepared with the background image.  Normally, at the end of your effect you might do something like this:

 

    :
    :
    if (TransparentBackground) return blahEffect;
    // otherwise mix with the source image
    MixEffect mixEffect = new MixEffect(deviceContext);
    mixEffect.Properties.Destination.Set(Environment.SourceImage);
    mixEffect.Properties.Source.Set(blahEffect); // your previous effect stream
    mixEffect.Properties.Mode.SetValue(MixMode.CompositeSourceOver);
    return mixEffect;
}

 

On 8/28/2024 at 2:35 PM, Red ochre said:

4. Can GpuDrawingEffects be added to a BitmapEffect and does this need to done in a separate method?
... say you wanted to mess with the colour of the Src pixel by pixel then 'draw' something on top using Direct2D.
If so how is this done.

 

There is a way, but this is outside the scope of my current tutorials.

 

Many of my effects do this.  It's done using a pixel shader.

 

 

 

On 8/28/2024 at 2:35 PM, Red ochre said:

5. Single-threaded effects; Previously 'OnSetRenderInfo' could be used for single threaded calculations to set 'global' variables, which could then be used in 'Render'.
Is this approach still possible and if not how should this be done?

 

Sure, this is still possible.

 

On the Build DLL screen, you can use the following options:

 

image.png

 

 

You can run CodeLab in these modes for development by using the Options screen:

 

image.png

 

On 8/28/2024 at 2:35 PM, Red ochre said:

6. From @BoltBait's tutorial I can draw ellipses and fill them with a SolidColorBrush but cannot fathom the values needed in the RadialGradientBrush.
Actually a more advanced tutorial on using paths and various types of brushes could be very useful!

 

Gradient Brush is a little more complicated than a Solid Color Brush...  This is for a GPU Drawing effect:

 

// You'll need to create a geometry from whatever you wish to render, for example text would look something like this:
IGeometry myGeometry = d2dFactory.CreateGeometryFromTextLayout(...

 

// Then, let's define our graident (this is 2 colors, but you can put more in there if you want)
GradientStop[] gradientStops =
{
    new GradientStop(0.0f, gradientFillFromColor),
    new GradientStop(1.0f, gradientFillToColor)
};
IGradientStopCollection spGradientStopCollection = deviceContext.CreateGradientStopCollection(
    gradientStops,
    GradientStopGamma.Linear,
    ExtendMode.Clamp);
// Create your brush. You need to specify two points that define the start and end of the gradient:
ILinearGradientBrush gradientBrush = deviceContext.CreateLinearGradientBrush(myGeometry.GetBounds().TopLeft, myGeometry.GetBounds().BottomLeft, spGradientStopCollection);

 

// finally, you fill the geometry
deviceContext.FillGeometry(myGeometry, gradientBrush);

 

Hope this helps!

 

😎 :beer: 

   

  • Like 1
  • Upvote 2
Link to comment
Share on other sites

Thanks both for your replies - I am finding this frustrating but do appreciate your help!

I will re-read your tutorials but example code would help.

 

@Rick Brewster

The effect I'm tinkering with is called 'Slinky' (in my pack). Originally I was thinking of just changing the range of some of the controls but thought I'd try converting it.

https://forums.getpaint.net/topic/28267-slinky-eccentric-ellipses-new-21st-april-2014/
It will need to be compiled in VS to set the range and default for the Vector2Double control. (codelab cannot do this).
If I can get it working with the new system I was thinking of adding filled ellipses and possibly experimenting with other shapes.

Here is the 10 year old VS code

 

Slinky.cs

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

  • Solution

This looks very convertible to a GpuDrawingEffect.

 

Some notes on the conversion:

  • EnvironmentParameters is now just Environment
  • To get the primary/secondary color, use Environment.PrimaryColor/SecondaryColor. The way this works will be slightly different in 5.1 but it will be the same property (your existing plugin DLL won't break, but you'll need to make changes if you recompile).
  • The OnDraw() method is called once. You won't have to worry about the "roi" or any of that. Your drawing commands will be recorded into something called a command list, and then PDN will do the work to render it into each tile that gets rendered.
  • Worth noting is that a command list is a type of image in Direct2D. Other types of images include bitmaps and effects. So think of an image as an abstraction for something that has a bounding rectangle and which produces pixels within that rectangle.
  • So any time you see a parameter or property of type IDeviceImage, think of terms of bitmap, effect, and command list.
  • You can create your own command lists. I'm not seeing a need for it in this particular effect though.
  • Pens do not exist in Direct2D, but brushes do. A pen is basically just a brush, a stroke width, and a stroke style all in one object. Most of the drawing commands, e.g. DrawRectangle(), will take a brush, stroke width, and stroke style.
  • A stroke style is created with the IDirect2DFactory::CreateStrokeStyle() method.
  • The only thing I see that I don't have in the new system is GetBilinearSampleWrapped(). For now, just access the pixels with the RegionPtr indexer and use % to wrap around. I can provide this method's code and you can copy+paste it, or if it's a popular enough request I can find a way to provide something built-in.
  • You can still access the source layer as a regular CPU-side bitmap. 
    • First, use Environment.GetSourceBitmapBgra32() which returns an IEffectInputBitmap<ColorBgra32>
    • Next, call the Lock method to get an IBitmapLock<ColorBgra32>
    • From the lock object, use AsRegionPtr() to get a RegionPtr<ColorBgra32>. This gives faster, simpler access to the bitmap's contents.
    • Like this:
    • using IEffectInputBitmap<ColorBgra32> sourceBitmap = Environment.GetSourceBitmapBgra32();
      using IBitmapLock<ColorBgra32> sourceLock = sourceBitmap.Lock(new RectInt32(0, 0, Environment.Document.Size));
      RegionPtr<ColorBgra32> sourceRegion = sourceLock.AsRegionPtr();
      ...
      ColorBgra32 color = sourceRegion[x, y];
    • However, note that if you're using ColorBgra32, the colors will not be linearized by default. So, probably better to use ColorRgba128Float instead of ColorBgra32 in the code above. There are still some growing pains here -- the underlying systems are stable, but our "culture" and our code samples are still migrating.
2 hours ago, Red ochre said:

3. The correct new way to change the destination background colour or image?

You can use deviceContext.Clear(). It can be called without a color, which will clear as transparent. Or you can do something like, deviceContext.Clear(LinearColors.Red) or any ColorRgba128Float of your choosing.

 

ColorRgba128Float takes the R, G, B, A as floats that should be in the range [0, 1]. You can also create a ColorBgra32 and there's an implicit cast to ColorRgba128Float. So ColorRgba128Float c = ColorBgra32.FromBgra(128, 128, 255, 255) would auto-convert to [ 0.502, 0.502, 1.0, 1.0 ]

 

Note that in your effect you will be working in linear gamma space. Previously you'd have been in companded (sRGB) gamma space. This becomes very important when pulling colors off of a color wheel control, or when reading the source bitmap. This will make more sense in PDN 5.1, as the color management facilities are fully baked. For now, @BoltBait has a lot of examples he can show where he's converting between ColorBgra32, SrgbColorA, LinearColorA, and ColorRgba128Float.

 

Anyway that's enough for now, see how far you get and then ask more questions ...

 

 

  • Like 1
  • Thanks 1
  • You're a Smart Cookie! 1

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

2 hours ago, Red ochre said:

5. Single-threaded effects; Previously 'OnSetRenderInfo' could be used for single threaded calculations to set 'global' variables, which could then be used in 'Render'.
Is this approach still possible and if not how should this be done?

For this effect I would not worry about this. OnDraw() (GpuDrawingEffect) and OnCreateOutput() (GpuImageEffect) are already called on a single thread, and the rest of the rendering process waits for them. So treat those as your init-and-render method.

  • Like 1

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

Thank you @BoltBait , and @Rick Brewster Both very informative!

 

I just rolled a few questions together into one thread. Single-theadedness is hopefully not relevant for 'Slinky', but probably is for other effects.
Mixing some form of 'drawing', previously G.D.I.+, with values from the src image is needed for effects like my 'Cuboids' and 'Facet', which also use gradient brushes.
So Many thanks @BoltBait for the example code... I do hope there is a geometry for a closed path... for me to discover!

 

Yes, plenty to read, hopefully understand and experiment with but I'm sure to get stuck, so may well be pestering you both again.
I do appreciate some of the advantages (and problems) of changing the effect system.
Hopefully this thread will be useful to others too.

Thanks again!

  • Upvote 2

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

17 hours ago, Red ochre said:

The MS tutorials seem to assume a much deeper knowledge of C# and C++ than I have and I'm not sure how relevant they are within a Pdn effect.

Eg. https://learn.microsoft.com/en-us/windows/win32/direct2d/how-to-create-a-radial-gradient-brush

 

There is (almost) a 1:1 mapping between the native Direct2D types and namings, and those for Paint.NET's wrappers.

 

Any interface starting with ID2D1 will just start with I over on the PDN side, e.g. ID2D1SolidColorBrush --> ISolidColorBrush

 

Similarly, D2D1_STRUCT_NAME would map to just StructName, e.g. D2D1_GRADIENT_STOP --> GradientStop

 

Some of the names are changed to have "device" in them, e.g. ID2D1Brush --> IDeviceBrush

 

ID2D1RenderTarget, ID2D1DeviceContext, and ID2D1DeviceContext1-6 are all contained within IDeviceContext. These had a small cascade on name changes elsewhere, such as D2DERR_RECREATE_TARGET mapping to RecreateDeviceContextException (you won't have to deal with that one). Also, ID2D1BitmapRenderTarget maps to ICompatibleDeviceContext (because there's already an IBitmapDeviceContext, which is using the name in a better way). etc.

 

The point is that while Microsoft's C++ examples are weird to read, it is very translatable/transcribable to PDN effect code once you get used to it.

  • Upvote 1

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

The only thing in this thread that I can help with is:

 

9 hours ago, Red ochre said:

The quotes are Rick's from E.E.R.'s Box Outlining thread. ( I don't even know how to use quotes from different threads in this forum! 🙄)

 

1. In the original thread, select the text you want to quote and click the Quote selection popup in the normal way. This will create the quote in the Reply box in that thread.

 

2. In the Reply box press Ctrl+A then Ctrl+C to select and copy the quote. Then Delete to discard it.

 

3. Start your new thread and do Ctrl+V to paste the quote. The forum software will paste all the formatting, so that the date-time, the author, and the link arrow to the original thread will be preserved:
image.png
4. Repeat for any additional quotes you need to include.



--
Aside: Reading @Rick Brewster's and @BoltBait's replies to your questions leaves me in awe of the effort that has been put into the design and development of the paint.net GPU rendering system and the API to utilise it. Amazing work.

 

 

  • Thanks 1
Link to comment
Share on other sites

I've just posted my learnings thus far in the Box Outlining thread. I'll repeat the code here, because I wrote myself some pertinent notes...

 

Spoiler
// Name: Box Outlining GPU
// Submenu: test
// Author:
// Title:
// Version:
// Desc:
// Keywords:
// URL:
// Help:

// For help writing a GPU Drawing plugin: https://boltbait.com/pdn/CodeLab/help/tutorial/drawing/

#region UICode
IntSliderControl Amount2 = 25; // [15,100] Size
IntSliderControl Amount3 = 0; // [-100,100] X position
IntSliderControl Amount4 = 0; // [-100,100] Y position
#endregion

protected override unsafe void OnDraw(IDeviceContext deviceContext)
{
    // Preserve the background image by drawing (copying?) the source image to the output canvas (pointed to by "deviceContext")
    deviceContext.DrawImage(Environment.SourceImage);  

    // Gets the current selection from the environment and puts it into the "selection" integer rectangle variable. 
    // References the current selection, if there is one. If there isn't one, it returns the entire layer.
    RectInt32 selection = Environment.Selection.RenderBounds;

    // variables
    int boxSize = Amount2/2;
    int halfBoxSize = boxSize/2;
    int thickness = 2;
    int rndHeight, rndWidth;
    int minSpacing = (int)Math.Sqrt(boxSize);
    int maxSpacing = minSpacing*3;
    int wOffset, hOffset;

    // define any brushes and stroke styles
    ISolidColorBrush fillBrush = deviceContext.CreateSolidColorBrush(LinearColors.LightGray);
    ISolidColorBrush outlineBrush = deviceContext.CreateSolidColorBrush(LinearColors.Black);
    IStrokeStyle boxStrokeStyle = deviceContext.Factory.CreateStrokeStyle(StrokeStyleProperties.Default);

    // setup drawing mode
    deviceContext.AntialiasMode = AntialiasMode.Aliased;  // or .PerPrimitive

    // Initalize and seed the random number generator
    Random rnd = new Random();

    // The next four lines allow polling of the source image.A value
    // Rick said .....use Environment.GetSourceBitmapBgra32(), which will give you an IEffectInputBitmap<ColorBgra32>
    IEffectInputBitmap<ColorBgra32> sourceImage = Environment.GetSourceBitmapBgra32();
    RectInt32 rect = new RectInt32(0,0,sourceImage.Size);    

    // .......then call Lock() to get the IBitmapLock<ColorBgra32>().    
    IBitmapLock<ColorBgra32> myLock =  sourceImage.Lock(rect); 
   
    // .....then use AsRegionPtr() to get it as a RegionPtr<ColorBgra32> 
    RegionPtr<ColorBgra32> myRegion = myLock.AsRegionPtr();

    // It is myRegion[x,y] that can be examined for the alpha value. 
    // Note this technique is not valid for checking the RGB values because of color management. Here's how Rick explained it....
    //  "Note that this works fine for querying the alpha channel, as the alpha channel is invariant with respect to color space (sRGB vs. linear scRGB vs. whatever other 
    //   color profile they might have on the image!). If you want to query the RGB values, they won't line up with the color space you're using in the GPU effect, and it'll 
    //   take some extra work to determine the right thing to do. So that's a topic for another time."
    //
    // A GPUDrawing effect has a main function called OnDraw() with color type LinearColors
   
    // Step across the selection (or layer if no selection is active), rendering boxes where the source pixel is opaque.
    for (int y = selection.Top; y < selection.Bottom; y+= boxSize+1)
    {
        for (int x = selection.Left; x < selection.Right; x+= boxSize+1)
        {
            //if source pixel is opaque
            if (myRegion[x, y].A == 255)
                {
                rndWidth = rnd.Next(minSpacing, maxSpacing);
                rndHeight = rnd.Next(minSpacing, maxSpacing);
                wOffset = rndWidth+halfBoxSize-Amount3+3;
                hOffset = rndHeight+halfBoxSize-Amount4+2;
                rndWidth *=3;
                rndHeight *=3;
                // draw a filled rectangle, followed by an outline to get a two-toned box
                deviceContext.FillRectangle(x-wOffset, y-hOffset, boxSize+rndWidth, boxSize+rndHeight, fillBrush);
                deviceContext.DrawRectangle(x-wOffset, y-hOffset, boxSize+rndWidth, boxSize+rndHeight, outlineBrush, thickness, boxStrokeStyle);
                }
        }
    }

    //    CompositeEffect compositeEffect = new CompositeEffect(deviceContext);
   //     compositeEffect.Properties.Sources.Add(this.Environment.SourceImage);
   //     compositeEffect.Properties.Mode.SetValue(CompositeMode.SourceOver);

}

 

 

  • Thanks 1
  • Upvote 1
Link to comment
Share on other sites

On 8/29/2024 at 6:35 AM, Red ochre said:

4. Can GpuDrawingEffects be added to a BitmapEffect and does this need to done in a separate method?
... say you wanted to mess with the colour of the Src pixel by pixel then 'draw' something on top using Direct2D.

 

Classic Effect + GDI drawing -> Bitmap Effect + D2D drawing is almost 1 to 1 conversion.

 

Surface -> IBitmap

ColorBgra -> ColorBgra32

GDI drawing -> D2D drawing

 

BoltBait's tutorial covers the rest so just GDI -> D2D part.

 

GDI+

surface ??= new Surface(src.Size);
using var gfx = new RenderArgs(surface).Graphics;
using var pen = new Pen(color, width);
surface.Clear();
gfx.SmoothingMode = SmoothingMode.AntiAlias;
gfx.DrawCurve(pen, points);

 

D2D

bitmap ??= Environment.ImagingFactory.CreateBitmap<ColorPrgba128Float>(Environment.Document.Size);
using var bdc = Services.GetService<IDirect2DFactory>().CreateBitmapDeviceContext(bitmap);
using var brush = bdc.CreateSolidColorBrush(color);
using (bdc.UseBeginDraw()) {
    bdc.Clear();
    bdc.DrawCurve(points, brush, width);
}

 

D2D only takes pre-multiplied bitmap so you need to use either ColorPrgba128Float or ColorPbgra32.

 

You still need to blend the result with the src and copy to the dst, but I don't know if there is a simple way to do this when the pixel formats are different.

This is a relatively simple thing to do in a GPU effect, but I was doing per pixel blending as a part of the per pixel rendering in my CPU effect.

  • Thanks 1
Link to comment
Share on other sites

@Ego Eram Reputo and @_koh_ thanks both for your input.

 

I'm making good progress converting the code as a GPU drawing effect. I did have problems with the colour
change from 'companded' to 'linear' but have found a conversion that seems to give results equal to the original code:

//psuedocode
ColorBgra oldCol;//was used in G.D.I.+ pens etc
ColorRgba128Float newCol;// to be used in D2D drawEllipse
byte oB = oldCol.B;byte oG = oldCol.G;byte oR = oldCol.R;
float nB = Math.Pow((float)(oB)/255f),2.2);
float nG = Math.Pow((float)(oG)/255f),2.2);
float nR = Math.Pow((float)(oR)/255f),2.2);
newCol = new ColorRgba128Float(nR,nG,nB,1f);//opaque

I still have some (presumably) scope problems accessing src pixel values within the loop...
 but I hope to be able to work those out. I will post my new code here when happy with it but
am certainly making better progress than before asking!... thanks all.

 

@_koh_ your explanation and example code are very welcome and I intend to try the Bitmap effect approach later too.

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

36 minutes ago, Red ochre said:

I did have problems with the colour
change from 'companded' to 'linear' but have found a conversion that seems to give results equal to the original code

 

Never used GpuDrawingEffect but having this method in your class sets the color space original instead of the linear I believe.
And yeah, if you want to mimic built-in brush and layer blending, you have to use the original color space.

 

protected override void OnInitializeRenderInfo(IGpuDrawingEffectRenderInfo renderInfo) {
    renderInfo.ColorContext = GpuEffectColorContext.WorkingSpace;
    base.OnInitializeRenderInfo(renderInfo);
}
  • Thanks 1
Link to comment
Share on other sites

4 hours ago, Red ochre said:
float nB = Math.Pow((float)(oB)/255f),2.2);
float nG = Math.Pow((float)(oG)/255f),2.2);
float nR = Math.Pow((float)(oR)/255f),2.2);

 

Not necessary! Cast your ColorBgra32 to SrgbColorA, then cast to LinearColorA, which then implicitly casts to ColorRgba128Float.

 

You can also use ColorBgr24 which, for instance, implicitly casts to ColorBgra32 with A=255. You can also use SrgbColor (no "A") and LinearColor (no "A") which do not have alpha. And ColorRgb96Float is the non-alpha supporting version of ColorRgba128Float

 

e.g.

ColorRgb96Float n = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(oB, oG, oB)

 

You can also use the code @_koh_ mentions to switch the color context to WorkingSpace. However, this does come at the cost of reducing the quality and correctness of blending and sampling. It is highly advantageous to work in linear gamma space, which is why it's the default. There are still some "teething issues" with the whole color management thing.

  • Thanks 1

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

Thanks for that, it makes the code much tidier.

 

Here is a zip with the .dll and codelab.cs (I have'nt built via VS, or added a sample image yet).

SlinkyPlus2_04.zip

Please let me know if you find any bugs.

I'll show the code in hidden code block below too.

Many thanks for all the help... I think I have a much better idea of what I'm doing with new system now!

Spoiler
// Name:SlinkyPlus
// Submenu:Iterative lines
// Author:John Robbins (Red ochre)
// Title:SlinkyPlus 2.04         Sept 2024       Red ochre
// Version:2.04
// Desc:Draws varying shapes along a path.
// Keywords:Circle, Ellipse, Triangle, Rectangle, Square, Polygon
// URL:
// Help:

// For help writing a GPU Drawing plugin: https://boltbait.com/pdn/CodeLab/help/tutorial/drawing/

#region UICode
IntSliderControl Amount1 = 25; // [2,1000] Number of shapes
PanSliderControl Amount2 = new Vector2Double(0.000, 0.000); // Start position
IntSliderControl Amount3 = 100; // [0,1000] Start size
DoubleSliderControl Amount4 = 0; // [-10,10] Wide....................Start shape......................Tall
PanSliderControl Amount5 = new Vector2Double(0.000, 0.000); // End position
IntSliderControl Amount6 = 100; // [0,1000] End size
DoubleSliderControl Amount7 = 0; // [-10,10] Wide....................End shape........................Tall
ListBoxControl Amount8 = 0; // Curve type|semi-circular|semi-sinosoidal|sinosoidal
DoubleSliderControl Amount9 = 0; // [-200,200] Curvature %
IntSliderControl Amount10 = 10; // [1,100] Line width
ListBoxControl Amount11 = 0; // Colour options|YRMBCG Rainbow|CBMRYG Rainbow|MRYGCB Rainbow|Primary|Secondary|Primary to Secondary|Colours from source image
CheckboxControl Amount12 = true; // Clear background
#endregion

protected override unsafe void OnDraw(IDeviceContext deviceContext)
{
    deviceContext.DrawImage(Environment.SourceImage);
    RectInt32 selection = Environment.Selection.RenderBounds;
   
 //Safely get the src image to use for colour option 6 later
    using IEffectInputBitmap<ColorBgra32> sourceBitmap = Environment.GetSourceBitmapBgra32();
    using IBitmapLock<ColorBgra32> sourceLock = sourceBitmap.Lock(new RectInt32(0, 0, Environment.Document.Size));
    RegionPtr<ColorBgra32> sourceRegion = sourceLock.AsRegionPtr();
 // Introducing option to clear the background as the above code seems to render the background to dst too.
    if(Amount12){deviceContext.Clear();}
    
    ColorBgra PC = Environment.PrimaryColor;//thanks Rick!
    ColorRgb96Float Pcol = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(PC.B, PC.G, PC.R);//now in linear colour space
    ColorBgra SC = Environment.SecondaryColor;
    ColorRgb96Float Scol = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(SC.B, SC.G, SC.R);
    int lineW = Amount10;
    int Stop = selection.Top;
    int Sbott = selection.Bottom;
    int Sleft = selection.Left;
    int Srite = selection.Right;
    int H = Sbott - Stop;
    int W = Srite - Sleft;
    double dx = W / 2;
    double dy = H / 2;
    double Pi = Math.PI;
    int LineNum = Amount1;
 //start
    double A1x = Amount2.X; double A1y = Amount2.Y;//CP1 
    float staS = Amount3;//start size
    double starat = Amount4 / 10;//stErat = start Ellipse ratio
    float Swidth = staS; if (starat > 0) { Swidth = (float)(staS * (1 - starat)); }
    float Sheight = staS; if (starat < 0) { Sheight = (float)(staS * (1 + starat)); }
 //end
    double A2x = Amount5.X; double A2y = Amount5.Y;//CP2 - end
    float endS = Amount6;// end size
    double endrat = Amount7 / 10;// = end Ellipse ratio
    float Ewidth = endS; if (endrat > 0) { Ewidth = (float)(endS * (1 - endrat)); }
    float Eheight = endS; if (endrat < 0) { Eheight = (float)(endS * (1 + endrat)); }
 // NEW need half values of start and end widths and heights to offset positions
    float halfstaW = Swidth/2f;float halfstaH = Sheight/2f;
    float halfendW = Ewidth/2f;float halfendH = Eheight/2f;
 // differences
    float Wdiff = Ewidth - Swidth;
    float Hdiff = Eheight - Sheight;
 // curvature
    double curvrat = Amount9 / 100;
 /* Pan slider defaults and range cannot be edited in codelab 6.12.
    So here, if both pan sliders are at zero I set the start and end coords.
    When compiled in VS this can go and these defaults and a range of 2 to -2 set in OnCreatePropertyCollection()
 */
    if (A1x == 0 && A1y == 0 && A2x == 0 && A2y == 0){ A1x = -0.5; A1y = -0.5; A2x = 0.5; A2y = 0.5; }

 // The ellipse are drawn from the centre coord BUT the major and minor radii must be factored in for position
    float Cp1x = (float)(Sleft + halfstaW + ((A1x + 1) * dx));
    float Cp1y = (float)(Stop + halfstaH + ((A1y + 1) * dy));
    float Cp2x = (float)(Sleft + halfendW + ((A2x + 1) * dx));
    float Cp2y = (float)(Stop + halfendH + ((A2y + 1) * dy));
 // define your brush and stroke style
    ISolidColorBrush PriBrush = deviceContext.CreateSolidColorBrush(PC);
    IStrokeStyle PriStrokeStyle = deviceContext.Factory.CreateStrokeStyle(StrokeStyleProperties.Default);
 // setup drawing mode
    deviceContext.AntialiasMode = AntialiasMode.PerPrimitive;  // or .Aliased
 // Create points for curve, the path the ellipses take.
    Point2Float CP1 = new Point2Float(Cp1x, Cp1y);//start ellipse centre

    float Xdiff = Cp2x - Cp1x;
    float Ydiff = Cp2y - Cp1y;
    float LN = LineNum - 1;
    float xdiv = Xdiff / LN;
    float ydiv = Ydiff / LN;
    float Wdiv = Wdiff / LN;
    float Hdiv = Hdiff / LN;

    double dist = Math.Sqrt((Xdiff * Xdiff) + (Ydiff * Ydiff));
    float Ddiv = (float)(dist / LN);// minimum number of ellipses now 2 so now no div by zero worries

    double stangle = Math.Atan2(Ydiff, Xdiff);
    double tangangle = stangle + (Pi / 2);

// The loop
for (int N = 0; N < LineNum; N++)
            {
               
                //curvature
                double curve = 0;
                double Drat2 = (double)(N) / (double)(LineNum);
                double r = dist / 2;

                switch (Amount8)
                {
                    case 2://semi-circular
                        curve = r * Math.Sqrt(Math.Sin(Drat2 * Pi)) * curvrat;
                        break;
                    case 0://semi-sinusoidal
                        curve = r * Math.Sin(Drat2 * Pi) * curvrat;
                        break;
                    case 1://sinusoidal
                        curve = r * Math.Sin(Drat2 * Pi * 2) * curvrat;
                        break;
                }

                float Xcurve = (float)(curve * Math.Cos(tangangle));
                float Ycurve = (float)(curve * Math.Sin(tangangle));


                float Wrad = (float)(Swidth + (N * Wdiv));
                float Hrad = (float)(Sheight + (N * Hdiv));
                float mcx = (float)(Cp1x + (N * xdiv)) - (Wrad / 2) + Xcurve;
                float mcy = (float)(Cp1y + (N * ydiv)) - (Hrad / 2) + Ycurve;
                Point2Float centre = new Point2Float(mcx, mcy);//ellipse centres
                //ellipse centres can be off canvas BUT cannot sample src pixels that don't exist or out of bounds exception!
                int mcxi = (int)mcx; int mcyi = (int)mcy;// these used to access sourceRegion so effectively clamped below.
                if(mcxi < selection.Left){mcxi = selection.Left;}
                if(mcxi >= selection.Right){mcxi = selection.Right;}
                if(mcyi < selection.Top){mcyi = selection.Top;}
                if(mcyi >= selection.Bottom){mcyi = selection.Bottom;}
                //Colour options
                double Drat = (double)(N + 1) / (double)(LineNum);
                //for rainbows
                double C1d = (Math.Sin(Drat * Pi * 2) * 128) + 128; if (C1d > 255) { C1d = 255; }
                double C2d = (Math.Sin((Drat * Pi * 2) + (Pi * 0.666666)) * 128) + 128; if (C2d > 255) { C2d = 255; }
                double C3d = (Math.Sin((Drat * Pi * 2) + (Pi * 1.333333)) * 128) + 128; if (C3d > 255) { C3d = 255; }

                byte C1 = (byte)(C1d);// calculating as doubles then casting to byte is long winded but more readable (for me).
                byte C2 = (byte)(C2d);
                byte C3 = (byte)(C3d);
                

                switch (Amount11)
                {
                    case 0://rainbow YRMBCG
                        ColorRgb96Float RBG = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(C3, C2, C1);// Thanks Rick!
                        PriBrush.Color = RBG;// The casts above convert from sRGB (companded) to linear colour space that D2D expects.
                        break;
                    case 1://rainbow CBMRYG
                        ColorRgb96Float BRG = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(C1, C2, C3);
                        PriBrush.Color = BRG;
                        break;
                    case 2://rainbow MRYGCB
                        ColorRgb96Float RGB = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(C2, C3, C1);
                        PriBrush.Color = RGB;
                        break;
                    case 3://Primary
                        PriBrush.Color = Pcol;
                        break;
                    case 4://Secondary
                        PriBrush.Color = Scol;
                        break;
                    case 5://Primary to Secondary
                        double iDrat = 1 - Drat;
                        C1 = (byte)((iDrat * PC.R) + (Drat * SC.R));//use companded colorspace to mix
                        C2 = (byte)((iDrat * PC.G) + (Drat * SC.G));
                        C3 = (byte)((iDrat * PC.B) + (Drat * SC.B)); 
                        PriBrush.Color = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(C3, C2, C1);//Convert to linear for D2D brush
                        break;
                    case 6://use source colours
                        //Must be protected from out of bounds exeptions, see line 150
                        // perhaps there is 'clamped' option?... but could not find it with intellisense.
                        ColorBgra32 srcCol = sourceRegion[mcxi, mcyi];//clamped to canvas
                        PriBrush.Color = (LinearColor)(SrgbColor)ColorBgr24.FromBgr(srcCol.B, srcCol.G, srcCol.R);
                        break;
                }// end colour switch block

              //draw the ellipses
              Ellipse E = new Ellipse(mcx,mcy,Wrad,Hrad);//not clamped coords
              deviceContext.DrawEllipse(E,PriBrush,lineW,PriStrokeStyle);//Ellipse,Brush,Line width, stroke style            
              
            }// end loop
}// end OnDraw

 

 

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

Hmmm...

    deviceContext.DrawImage(Environment.SourceImage); // This line renders the background to dst

So, you could combine, like this:

 

    if (!Amount12) 
    {
      deviceContext.DrawImage(Environment.SourceImage);
    }

So, no need for this:

 

if(Amount12){deviceContext.Clear();}

 

Remember, if you don't draw the source image to the deviceContext, your image will start out transparent.

 

  • Upvote 2
Link to comment
Share on other sites

On 8/31/2024 at 4:32 AM, Rick Brewster said:

this does come at the cost of reducing the quality and correctness of blending and sampling.

 

How brush color interact image color is a human perception thing in my view, so using sRGB brush is my preferable choice so far.
One thing I'm sure is manually using linear brush would be a bit cumbersome. Using sRGB brush is kinda like using log scale for convenience.

Link to comment
Share on other sites

That annoying bloke with the ellipses is back again! 😁

 

How do I rotate an ellipse?
Here are the D2D C++ links but I cannot find anything in codelab's intellisense that looks likely.
Are these methods available in codelab? (under another name).

 

https://learn.microsoft.com/en-us/windows/win32/learnwin32/applying-transforms-in-direct2d
https://learn.microsoft.com/en-us/windows/win32/api/d2d1helper/nf-d2d1helper-matrix3x2f-rotation

 

Ideally I would rotate an ellipse 'E' by a small angle on each iteration to change from a start angle to an end angle?

Thanks for your patience.

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

2 hours ago, Red ochre said:

How do I rotate an ellipse?

 

You can use deviceContext.Transform for this.

But this transforms whole image so setting the center of the ellipse (0,0) and moving the ellipse through the transform matrix might be simpler.

 

Also you can use Math.Min(), Math.Max(), Math.Clamp() for if (x > a) x = a kind of things.

Link to comment
Share on other sites

18 minutes ago, _koh_ said:

You can use deviceContext.Transform for this.

No, I can't?... I don't see that option?... that's why I asked.

18 minutes ago, _koh_ said:

Also you can use Math.Min(), Math.Max(), Math.Clamp() for if (x > a) x = a kind of things.

Yes, I could write the formula for a Bitmap ellipse but I'm trying to use D2D?

https://forums.getpaint.net/topic/23194-squirkle/

I'll get my coat.😕

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

7 minutes ago, Red ochre said:

No, I can't?... I don't see that option?... that's why I asked.

 

Umm. I have that property in my CodeLab.

 

image.png.f6e658b1e16a7a6f6db525843cf46e1c.png

 

8 minutes ago, Red ochre said:

Yes, I could write the formula for a Bitmap ellipse but I'm trying to use D2D?

 

I'm saying you can do this to clamp sample coordinate.

 

int mcxi = (int)Math.Clamp(mcx, selection.Left, selection.Right);
int mcyi = (int)Math.Clamp(mcy, selection.Top, selection.Bottom);
  • Thanks 1
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...