Jump to content
Paint.NET 5.1 is now available! ×

Recommended Posts

Posted

If you use the CodeLab plugin, you will note that it offers some sample scripts, but a requirement for these plugins is not correctly explained. Your renderer routine should be declared like this:

 void Render(
   PaintDotNet.Surface dstSurf,
   PaintDotNet.Surface srcSurf,
   System.Drawing.Rectangle rectDraw)

It is critically important to take the third parameter (here named rectDraw) into account, i.e. you must not draw on the destination surface outside of this box, otherwise you will see some undesirable "band" effects (it will affect the results of concurrent threads working on the same surface to draw in the surrounding bands).

I think that this is nearly a bug of Paint.Net (and one source of bugs in many complex filters where those bands become visible) that can cause undesirable effects of your custom effect built with "CodeLab"; you code must be adapted to effectively clip its rendering.

In addition, you must not read any pixel outside this box from the destination surface (because its content can change without notice as other concurrent threads running your Render routine are also drawing to these areas). Only the source surface is safe and can be read everywhere (but you must not write to the source surface because concurrent threads, also running your Render routine, will also need to read also from the same unmodified image). You can however safely write and read pixels (any number of times) in the destination surface as long as they are in the specified rectangle.

If you consider this parameter, you can also significantly improve the rendering time of your custom filter (by avoiding unnecessary computing on areas that should not be drawn multiple times and that must be kept untouched).

The following program demonstrate how your plugin will receive rendering orders with rectDraw using variable size (but it does not demonstrate which thread number is drawing each band, I don't know if the list of rendering threads and the active thread number it is accessible from PaintDotNet.Effect's environment):

Hidden Content:
class Renderer {
   private PaintDotNet.Surface dstSurf;
   private System.Drawing.Rectangle rectDraw;

   public Renderer(
       PaintDotNet.Surface dstSurf,
       System.Drawing.Rectangle rectDraw) {
       this.dstSurf  = dstSurf;
       this.rectDraw = rectDraw;
       this.openPath = false;
   }

   /* Fill the rectangle from (x, y) inclusive to (x2, y2) exclusive
    * (if both points are equal, fill nothing). */
   public void rectFill(PaintDotNet.ColorBgra c, int x, int y, int x2, int y2) {
       int x1, y1;
       if (x > x2) { x1 = x2 + 1; x2 = x + 1; } else x1 = x;
       if (y > y2) { y1 = y2 + 1; y2 = y + 1; } else y1 = y;
       // Implementation note: we MUST NOT draw outside this bounding box
       // (otherwise random bands will be visible)
       int X1 = this.rectDraw.Left, X2 = this.rectDraw.Right,
           Y1 = this.rectDraw.Top,  Y2 = this.rectDraw.Bottom;
       if (x1 < X2 && x2 > X1 && y1 < Y2 && y2 > Y1) {
           // At least some part is in the bounding box, cap to that box
           if (x1 < X1) x1 = X1; if (x2 > X2) x2 = X2;
           if (y1 < Y1) y1 = Y1; if (y2 > Y2) y2 = Y2;
           // Fill the remaining part in that surface
           PaintDotNet.Surface dstSurf = this.dstSurf;
           for (y = y1; y < y2; y++)
               for (x = x1; x < x2; x++)
                   dstSurf[x, y] = c;
       }
   }

   /* Plot the border of rectangle from (x, y) inclusive to (x2, y2) exclusive
    * (if both points are equal, plot nothing). */
   public void rect(PaintDotNet.ColorBgra c, int x, int y, int x2, int y2) {
       if (x != x2 && y != y2) {
           int tmp;
           if (x > x2) { tmp = x + 1; x = x2 + 1; x2 = tmp; }
           if (y > y2) { tmp = y + 1; y = y2 + 1; y2 = tmp; }
           rectFill(c, x, y, x2, ++y);
           if (y != y2) {
               rectFill(c, x, --y2, x2, y2 + 1);
               if (y != y2) {
                   rectFill(c, x++, y, x, y2);
                   if (x != x2)
                       rectFill(c, x2 - 1, y, x2, y2);
               }
           }
       }
   }
};

void Render(
   PaintDotNet.Surface dstSurf,
   PaintDotNet.Surface srcSurf,
   System.Drawing.Rectangle rectDraw)
{
   PaintDotNet.Effects.EffectEnvironmentParameters env;
   PaintDotNet.ColorBgra cFg, cBg;
   //float bw;
   //PaintDotNet.PdnRegion rgnSel;
   //System.Drawing.Rectangle rectSel;

   env = this.EnvironmentParameters;
   cFg = env.PrimaryColor;
   cBg = env.SecondaryColor;
   //bw = env.BrushWidth;
   //rgnSel = env.GetSelection(dstSurf.Bounds);
   //rectSel = rgnSel.GetBoundsInt();

   Renderer draw = new Renderer(dstSurf, rectDraw);
   // display draw bands
   int x1 = rectDraw.Left;
   int y1 = rectDraw.Top;
   int x2 = rectDraw.Right;
   int y2 = rectDraw.Bottom;

   draw.rectFill(cBg, x1, y1, x2, y2);
   PaintDotNet.ColorBgra c = new PaintDotNet.ColorBgra();
   c.G = (byte)(y1*51);
   c.B = (byte)(y1*53);
   c.R = (byte)(y1*59);
   draw.rect(c, x1, y1, x2, y2);
}

Run it from a new empty image, it will fill it with a series of rectangle with a border of varying color, filled with white (the current background color selected in the main interface of Paint.net. Note that the rectangles have variable height (this depends on the number of processor cores you have, and on the width of the surface to render, and on the system load on each CPU, because Paint.Net splits the rendering into multiple tasks to perform the effect in the selected rectangle, which by default is the whole image).

If you drop the test in rectFill() of the limits for rectDraw, you will see that the rectangles are partly overriden, but not completely. And that the final image you get when running the plugin may change over time, even when your CPU has a single CPU core (generally the first band at the top is higher than the remaining ones).

One consequence of the "banding" effect produced by Paint.Net when it calls your Render() function (and demonstrated here) is that almost all Render() routines should not make the computed colors of pixels dependant of the position of the drawing rect (whose position in the image or in the selection area if you use it is unpredictable)... unless you really want to make those bands visible as above, where the position of the rectangle to draw is used to compute the color of the 1-pixel borders of rectangles, in order to make them distinctive.

In all cases, your Render routine must strictly avoid drawing outside bands, or you'll see unpredictable overlapping effects (due to concurrent threads), notably if your render routine renders some pixels in multiple overlapping passes (for example filling the whole rectangular area in white, then drawing some other parts in black) !

This undesirable effect will occur even when running on a single core CPU where only one thread is active : why does Paint.Net splits the area to draw in multiple bands is a mystery for me: it should use a single rendering thread, and not more than the number of active CPU cores... But may be it uses the capabilities of the GPU to run your render routine on it with parallel threads (I don't know the internals of Paint.Net, if it can recompile your dotNet Render() routine into a shader routine for the GPU)...

----

Now suppose you want to draw straight segments in your plugin. You could think about using a Bresenham algorithm to compute the position of pixels between two positions: as long as the positions of the two limiting vertices are effectively in the image limits, you could think that you can draw anywhere on the image, ignoring the third "rectDraw" parameter in the plugin. DON'T DO THAT ! This program will show you what you can do to draw line segments fast, but still respect the rectangle (the actual rendering code is in the defined class "Renderer" which is slightly optimized, but not completely to demoonstrate the problem):

Here is the implementation of a generic Renderer class:

Hidden Content:
class Renderer {
   private PaintDotNet.Surface dstSurf;
   private System.Drawing.Rectangle rectDraw;

   public Renderer(
       PaintDotNet.Surface dstSurf,
       System.Drawing.Rectangle rectDraw) {
       this.dstSurf  = dstSurf;
       this.rectDraw = rectDraw;
       this.openPath = false;
   }

   /* Fill the rectangle from (x, y) inclusive to (x2, y2) exclusive
    * (if both points are equal, fill nothing).  */
   public void rectFill(PaintDotNet.ColorBgra c, int x, int y, int x2, int y2) {
       int x1, y1;
       if (x > x2) { x1 = x2 + 1; x2 = x + 1; } else x1 = x;
       if (y > y2) { y1 = y2 + 1; y2 = y + 1; } else y1 = y;
       // Implementation note: we MUST NOT draw outside this bounding box
       // (otherwise random bands will be visible)
       int X1 = this.rectDraw.Left, X2 = this.rectDraw.Right,
           Y1 = this.rectDraw.Top,  Y2 = this.rectDraw.Bottom;
       if (x1 < X2 && x2 > X1 && y1 < Y2 && y2 > Y1) {
           // At least some part is in the bounding box, cap to that box
           if (x1 < X1) x1 = X1; if (x2 > X2) x2 = X2;
           if (y1 < Y1) y1 = Y1; if (y2 > Y2) y2 = Y2;
           // Fill the remaining part in that surface
           PaintDotNet.Surface dstSurf = this.dstSurf;
           for (y = y1; y < y2; y++)
               for (x = x1; x < x2; x++)
                   dstSurf[x, y] = c;
       }
   }

   /* Plot the border of rectangle from (x, y) inclusive to (x2, y2) exclusive
    * (if both points are equal, plot nothing). */
   public void rect(PaintDotNet.ColorBgra c, int x, int y, int x2, int y2) {
       if (x != x2 && y != y2) {
           int tmp;
           if (x > x2) { tmp = x + 1; x = x2 + 1; x2 = tmp; }
           if (y > y2) { tmp = y + 1; y = y2 + 1; y2 = tmp; }
           rectFill(c, x, y, x2, ++y);
           if (y != y2) {
               rectFill(c, x, --y2, x2, y2 + 1);
               if (y != y2) {
                   rectFill(c, x++, y, x, y2);
                   if (x != x2)
                       rectFill(c, x2 - 1, y, x2, y2);
               }
           }
       }
   }

   /* Plot a line between (x, y) inclusive to (x2, y2) exclusive
    * (if both points are equal, plot nothing). */
   public void line(PaintDotNet.ColorBgra c, int x, int y, int x2, int y2) {
       int n, e;
       // Implementation note: we MUST NOT draw outside this bounding box
       // (otherwise random bands will be visible)
       int X1 = this.rectDraw.Left, X2 = this.rectDraw.Right,
           Y1 = this.rectDraw.Top,  Y2 = this.rectDraw.Bottom;
       PaintDotNet.Surface s = this.dstSurf;
       if ((x2 -= x) >= 0)
            if ((y2 -= y) >= 0)
                 if (x2 >= y2)
                      for (y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                          if (X1 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          x++; if ((e -= y2) <= 0) { e += x2; y++; } }
                 else for (x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                          if (X1 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          y++; if ((e -= x2) <= 0) { e += y2; x++; } }
            else if (x2 >= (y2 = -y2))
                      for (y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                          if (X1 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          x++; if ((e -= y2) < 0) { e += x2; y--; } }
                 else for (x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                          if (0 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          y--; if ((e -= x2) <= 0) { e += y2; x++; } }
       else /* Note: the tests of sign of e MUST invert the condition where it's zero,
               otherwise some pixel positions will depend on the direction of drawing */
            if ((y2 -= y) >= 0)
                 if ((x2 = -x2) >= y2)
                      for (y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                          if (X1 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          x--; if ((e -= y2) <= 0) { e += x2; y++; } }
                 else for (x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                          if (X1 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          y++; if ((e -= x2) < 0) { e += y2; x--; } }
            else if ((x2 = -x2) >= (y2 = -y2))
                      for (y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                          if (X1 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          x--; if ((e -= y2) < 0) { e += x2; y--; } }
                 else for (x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                          if (X1 <= x && x < X2 && Y1 <= y && y < Y2) s[x, y] = c;
                          y--; if ((e -= x2) < 0) { e += y2; x--; } }
   }

   private PaintDotNet.ColorBgra color, fill;

   public void setColor(PaintDotNet.ColorBgra c) {
       this.color = c;
   }

   public void setFill(PaintDotNet.ColorBgra c) {
       this.fill = c;
   }

   private bool openPath;
   private int xStart, yStart;
   private int xCurrent, yCurrent;

   public void closePath() {
       lineTo(this.xStart, this.yStart);
       this.openPath = false;
   }

   public void endPath() {
       if (!this.openPath) return;
       rectFill(this.color, this.xCurrent, this.yCurrent, this.xCurrent + 1, this.yCurrent + 1);
       this.openPath = false;
   }

   public void moveTo(int x, int y) {
       if (this.openPath) return;
       this.xStart = this.xCurrent = x;
       this.yStart = this.yCurrent = y;
       this.openPath = true;
   }

   public void lineTo(int x2, int y2) {
       if (!this.openPath) return;
       line(this.color, this.xCurrent, this.yCurrent, x2, y2);
       this.xCurrent = x2; this.yCurrent = y2;
   }

}

And here is an example of how you can use it (insert the class above in the code before this Render routine):

Hidden Content:
void Render(
   PaintDotNet.Surface dstSurf,
   PaintDotNet.Surface srcSurf,
   System.Drawing.Rectangle rectDraw)
{
   PaintDotNet.Effects.EffectEnvironmentParameters env;
   PaintDotNet.ColorBgra cFg, cBg;
   //float bw;
   //PaintDotNet.PdnRegion rgnSel;
   //System.Drawing.Rectangle rectSel;

   env = this.EnvironmentParameters;
   cFg = env.PrimaryColor;
   cBg = env.SecondaryColor;
   //bw = env.BrushWidth;
   //rgnSel = env.GetSelection(dstSurf.Bounds);
   //rectSel = rgnSel.GetBoundsInt();

   Renderer draw = new Renderer(dstSurf, rectDraw);
   int xMax = dstSurf.Width  - 1;
   int yMax = dstSurf.Height - 1;
   PaintDotNet.ColorBgra c = new PaintDotNet.ColorBgra();

   // background color
   c.R = (byte)192;
   c.G = (byte)192;
   c.B = (byte)192;
   c.A = (byte)255;
   draw.rectFill(c, 0, 0, xMax + 1, yMax + 1);

   // number of horizontal and vertical subdivisions of the surface rectangle
   const int N = 40;
   // draw a distinctive (and inversible) light color in one direction
   c.R = (byte)255;
   c.G = (byte)255;
   c.B = (byte)255;
   c.A = (byte)255;
   draw.setColor(c);
   for (int i = N; i >= 0; --i) {
       draw.moveTo(                 0,                  0);
       draw.lineTo(xMax              , yMax *      i  / N);
       draw.lineTo(                 0, yMax              );
       draw.lineTo(xMax *      i  / N,                  0);
       draw.lineTo(xMax              , yMax              );
       draw.lineTo(                 0, yMax *      i  / N);
       draw.lineTo(xMax              ,                  0);
       draw.lineTo(xMax *      i  / N, yMax              );
       draw.closePath();
       draw.moveTo(xMax *      i  / N,                  0);
       draw.lineTo(xMax              , yMax *      i  / N);
       draw.lineTo(xMax * (N - i) / N, yMax              );
       draw.lineTo(                 0, yMax * (N - i) / N);
       draw.closePath();
   }
   // Redraw in inverted dark color in the reverse direction:
   // There should be no light pixels remaining on the border of lines
   c.R = (byte)(~c.R);
   c.G = (byte)(~c.G);
   c.B = (byte)(~c.;
   c.A = (byte)(255);
   draw.setColor(c);
   for (int i = N; i >= 0; --i) {
       draw.moveTo(                 0,                  0);
       draw.lineTo(xMax *      i  / N, yMax              );
       draw.lineTo(xMax              ,                  0);
       draw.lineTo(                 0, yMax *      i  / N);
       draw.lineTo(xMax              , yMax              );
       draw.lineTo(xMax *      i  / N,                  0);
       draw.lineTo(                 0, yMax              );
       draw.lineTo(xMax              , yMax *      i  / N);
       draw.closePath();
       draw.moveTo(xMax *      i  / N,                  0);
       draw.lineTo(                 0, yMax * (N - i) / N);
       draw.lineTo(xMax * (N - i) / N, yMax              );
       draw.lineTo(xMax              , yMax *      i  / N);
       draw.closePath();
   }
}

Note that the code to draw lines is tuned so that lines will draw identically, independantly of their direction, and this is what the main Render() routine will test: you should NOT see any white pixels on the left or right of the black lines that are simply drawn in the reverse directions, over the grey surface.

You'll also note (if you set the constant N to a high value like 200, when your image size is about 799x599) that the expected dot patterns will not exhibit the perfect symetries of the "moiré" that you would expect when the image size has even coordinates (so that its center is on the center of a pixel): this is the effect of the dissimetry introduced on purpose here, to plot diagonal lines with exactly the same pixels independantly of their foreward or backward direction. If you make the tests of the sign of "e" equivalent between the cases where delta x is positive and the case where it is negative, you'll get symetric patterns, but the position of pixels drawn will not always be equivalent in the octants where the lines is nearer from the horizontal, you'll see white pixels remaining on the border of lines when the black lines are drawn in the reverse direction.

Now, if you comment out the test on X1/X2/Y1/Y2 within the "rectFill" primitive of the Renderer class, you will see the bad effect that the PaintDotNet "banding" can produce (because of concurrent threads): some parts will be drawn, some not, because the effective surface after each call of your Render() main function will be swapped concurrently and there's no protection of pixels outside of the rectDraw parameter: one thread running Render() routine will erase, with rectFill(), some parts of the lines currently drawn in another concurrent thread also running the same Render() routine.

This code (in the Renderer class provided) is also a much faster demonstration of how to draw lines or fill rectangles in a custom plugin: you can effectively test the rectDraw rectangle to avoid drawing multiple times in the current selected area (due to the way Paint.Net splits it into bands): you can set N=800 for example in the main Render() function, and it will effectively draw 16*800= 12800 segments over the surface.

Note: this code here does not use the selection rectangle: there's no need to test it in the Renderer class, but you can use it in the Render() function to compute the geometry of the draw or effect that your plugin will apply. As this Render() is just a demo for the Renderer class to see how you can use it safely, it does not use the selection (but it could).

If your Render function must use the current selection as a rectangle, make sure you first compute its bounding box to allow faster processing (and don't forget to clip this selection rectangle within the drawRect rectangle, and return immediately without drawing anything if it then becomes empty, using a clipping test like in the rectFill() primitive above).

But if the current selection is a random polygon and your custom effect uses complex transforms, it is highly recommended to first compute a rendering of this simple polygon within a new internal surface (with a single Alpha channel) created in the Render() function (and that you'll bound to the rectDraw rectangle to minimize the memory needed), and then draw your filter using the precomputed alpha surface: it will be much faster than testing the position of each pixel to see if it's within the selected area.

I just wonder why Paint.Net (or the CodeLab plugin) does not internally precomputes itself the selection polygon as an alpha plane, and why this selection surface is not passed as a parameter of the CodeLab plugin (this could be a null reference if the selection is a simple rectangle, in which case you'll just use the bounding box that you get from the environment). This should be a cute improvement of CodeLab.

Also I suggest that Paint.Net or CodeLab is fixed to assert that plugins will NOT be able to draw outside of the specified rectangle. Currently the surface outside of the band rectangle is NOT protected (so, as long as the coordinates given to index the dstSurf array are within the array limits, you can modify any portion of this surface; you can also read pixels from these locations, even if they are being updated in concurrent threads). So if you run your plugin on a multicore CPU, your effect may affect areas currently under modification by another thread using the same Render() function.

I hope that this sample code will help.

Notes: the Renderer.line() primitive can be optimized further to avoid scanning every pixels making a segment and see if they are within the bounds of the draw rectangle. But it would be more complex.

For this reason, don't compute a geometry in your main Render() function that will use coordinates outside of the image, even if it is clipped by the Renderer class using the bounds check of the draw rectangle). If all coordinates are in the image, you reasonably don't need to make this optimization for most geometries, as it will not give significant optimizations.

In addition, there would be the possible danger of incorrectly rounding the coordinates of the start and end of segments, and you would also have to compute the correct content of the "e" variable (which stores the double distance from the computed pixel position and the segment, and that is initialized here to the value of the smallest absolute delta between delta x and delta y, this variable controlling the effective line slope and where the secondary coordinate can be incremented or decremented): you would not only need to compute and correctly round the coordinates of the ends of the clipped segment, but you would also have to compute their distance from the theoretical segment that joins the initial unclipped segment.

Final note: this basic and quite fast Renderer class can be used to create all sorts of basic shapes, including the arrows that are shown in the CodeLab samples (and that are clearly NOT optimized to avoid multiple redraws, and that are drawing outside of the draw box !). With this class, it's really easy to draw circles or to render maths functions. However, this basic class won't allow you to create basic shapes that are not drawn with basic 1-pixel lines with a single solid color (possibly with a single alpha value) or to fill something else than rectangles aligned with the basic coordinates system : you'll need to add support for filling polygons, or you'll have to use the strategy of drawing many parallel lines spaced by at most 1 pixel (which may be slow if the area to fill is large and the lines are not parallel to the coordinates system).

In addition, it is not optimized to use the faster rectFill() when drawing horizontal and vertical lines in Renderer.line() ; that's something that is easy to add in this class: you can do it as a beginner's exercice, because it is easy, in this case, to clip the lines in the bounding box instead of testing each pixel.

But you can optimize the tests in this generic version, by drawing horizontal or vertical segments using RendererRectFill(), only when one of the two coordinate changes conditionally after testing the sign of "e" (you can do it yourself too and see how it improves the rendering time, because it avoids many conditional jumps in the compiled code).

(if you want the solution for the further optimized version that uses RectFill() as much as possible for drawing everything in the lineTo() primitive, look down in this thread).

Posted

Note: this Renderer class is a very simplified version of a more complete class that I am writing for rendering SVG images: it currently supports all SVG Tiny, and most of SVG 2.0 (with the exception of scripts that I'll implement later). It also supports strokes of variable width, and renders not just segments, but also other splines of variable orders (which I am currently optimizing for the orders 2 and 3), and it can fill polygones as well as shapes limited by splines.

It supports also fonts described in either TrueType or PostScript shapes (using quadratic or cubic curves). It also supports precise rendering of all conics, and I will include later the support for NURBS (which are the best splines to fit natural models). I also hope that, one day, the SVG graphics format and OpenType fonts will also support them instead of just the cubic Beziers which take too many curves to fit the basic conics and most math functions more exactly). For now I've just written specialized incremental routines for ellipses and for arcs of polynomial curves (with a "reasonnable" finite degree up to 256), drawn over an horizontal or vertical axis (and with a normal secondary axis).

Is also supports subpixels rendering (with 256 alpha values), optimized specially to use alpha values only where they are needed near the border of shapes, providing antialised lines. This version above does not implement it, but you can implement it easily from the code above by drawing into a temporary 1-bit surface with dimensions multiplied by 8 (for 64 alpha values, which you can remap to a byte value with a simple multiplication) or 16 (for 256 alpha values, requiring a 4-times bigger 1D surface), and using a cache for counting pixels. My more optimized version done not use a large 1-bit surface but computes the alpha value of each computed pixel directly in a single top-to-bottom, left-to-right pass over the destination surface, counting the matching 1-bit subpixels only where it is needed on the unaligned partially filled border of lines, and using a very small work buffer of only 16 bytes (for alpha counters in the same horizontal position), which is used only when there's at least one polygonal edge traversing it in the middle of the same 16x16x1bit subpixels square (making each final pixel), this small buffer being not used at all when I can predict that there will be either 0 or 256 1-bit subpixels covered by the surface to fill (according to the geometry).

I have also avoided all tesselisation of polygons into a list of convex trapezoids : this is not needed, even if in a prior version I used it : it does not scale very well with shapes described with curved borders, it requires more memory, it is in fact more complex to do correctly, and it is definitely not faster when you can compute the geometry incrementally with the correct (and much smaller) data structures directly from the original vertice coordinates only transformed into list of curved edges whose only the extremum points have been precomputed by splitting some edges, but without even computing where they may intersect (this will be determined incrementally directly when drawing, by reordering on the fly the list of active edges processed from top to bottom with a single scanline, and from right to left on the current scan line).

The antialiasing also supports for ClearType-like positioning of color planes (yes, this means that colors are transformed near the border of shapes, using monochromatic subpixels, because each color plane is computed separately as if they were slightly offsetted horizontally with about -1/3 pixel for red and about +1/3 pixel for blue, when using a LCD display with RGB subpixel order).

I will later optimize this ClearType-like rendering to take the color model into account (so that white points are more exactly preserved, something that even Windows does not consider in its implementation of ClearType subpixels, as it does not use correctly the separate gamma settings and the separate orientation of color planes which have separate angles, even when the 3 subpixels making a pixel are theoretically of equal size, something that is not completely true: the exact relative positioning of color planes is not really 1/3 pixel, as the green subpixel is a bit narrower than the blue one, and the red subpixel is a bit larger than the blue one, and this influences both the color model and the exact computing of alpha values for each color plane). But I will respect the sRGB color model strictly.

I'm also currently extending it to include 3D drawing primitives, using 4D transform matrixes to include the support of various projections).

Don't expect to see this posted here before long.

Posted

Some related suggestions to PaintDotNet or CodeLab developers:

Allow custom effect plugins to specify the properties they need and that should be precomputed in the PaintDotNet environment or in CodeLab environment : this custom properties routine should instruct PaintDotNet or CodeLab if it needs the selection area, and which format it can process, if it's not a simple bounding box. In that case, PaintDotNet or CodeLab could compute itself the selection area in an alpha plane (with the specified bitdepth, either 1-bit if the effect does not use antialiasing, or 8-bit if the effect uses antialiasing.

In that case, the Render() routines will have another parameter containing the precomputed selection surface as a single color plane with alpha values. As there will be several Render() routines, one for each surfaces combinator, they should be described using a standard interface, and the list of combinators will just have to create and return instances of each renderer, which should each implement a createInstance() routine returning an instance of one of the classes supporting one of the supported interfaces (for example an interface for a Render() routine combining two surfaces, another for an interface for just transforming 1 surface: if the renderer can modify one of the source surfaces, it must be also specified accordingly, because it will influence how the working threads will be ordered hierarchically, and will also influence how the effect can be applied to images in the PaintDotNet environment: how many of them must be selected to apply the effect, and how many additional images must be created).

May be a generic interface will just pass an ordered array of source surfaces in one parameter, and an array of target surfaces in the second parameter).

The scene to draw could also be described using multiple surfaces (possibly more than 2) managed by PaintDotNet itself, and to which a list of "combiners" (a specified Render() routine) will be applied hierarchically to produce the final surface(s) : the target of the effect could be multiple image layers, and there will also be one or several surfaces with one or more color planes with variable depths.

And PaintDotNet will split the rendering jobs conveniently. according to memory constraints per thread (the memory constraints apply to the size of bands in each described virtual surface, and a rendering routine will not be able to draw outside this virtual area, which will still be passed as the existing third parameter for the existing Render() routine.

Finally, there should be a way to specify that a Renderer() routine will only use the computing primitives that can be safely converted to the local GPU shader language, for much faster processing. Some dotNet operators or types or instructions should become unavailable, and there should be a way to use other surface formats than just the Bgra surfaces, to meet one of the supported GPU format surfaces.

(The DirectX/Direct3D API will make the rest, notably recompiling the DotNet-written Render() routines into a valid GPU pixel shader, if possible, or using the software pixel shaders implemented in DirectX).

An effect should also be allowed to control how the jobs will be split into bands: some parts of the scene may require more processing and more passes (combining more surfaces) than others, so there could also exist a property allowing a custom effect to specify how to split the scene into a minimum number of rectangles: PaintDotNet will then possibly subdivide these rectangles into bands when needed (and according to multithreading constraints or GPU capabilities requested by each combiner).

Currently, it just assumes that all the image surface has the same complexity, which is a poor strategy : rendering a tree on one part of the scene with many leaves is much more complex than rendering a house or a wall or a simple sphere. When the positions of objects on the image can be separated into areas where they do not collide, there's no need to allocate and compute large surfaces covering all the scene, when our effect can specify that the final image has areas with a simpler structure, and a more complex rendering routine, using more planes for computing the transparent areas to combine, will be used only on the bounding rectangles where they effectively collide.

Posted

This is for example how you can optimize the line() primitive in the Renderer class, which avoids testing each individual pixel to see if they are in the drawing rectangle. In this version, all the modifications in the destination image, and clipping tests are within rectFill(), which is also used directly for drawing horizontal and vertical segments.

For simplicity and brievity, the calls to rectFill() are not inlined here, and you may experience worse performance than with the previous version, mostly because of the overhead of method calls, but also because rectFill() still needs to read some class members which do not change while drawing lines, and that could be cached in local variables (the reference to the dest surface and the 4 coordinates of the drawRect). a true optimization would have to inline these 19 calls to RectFill(), or at least the 8 calls within the each for() loop used to draw diagonal segments in one of the 8 octants.

Hidden Content:
   public void line(PaintDotNet.ColorBgra c, int x, int y, int x2, int y2) {
       int n, e, o;
       if ((x2 -= x) == 0) rectFill(c, x, y, x + 1, y2);
       else if (x2 > 0)
            if ((y2 -= y) == 0) rectFill(c, x, y, x + x2, y + 1);
            else if (y2 > 0)
                 if (x2 >= y2) {
                    for (o = x, y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                       x++; if ((e -= y2) <= 0) { e += x2; rectFill(c, o, y, o = x, ++y); }
                    } rectFill(c, o, y, x, y + 1);
                 } else {
                    for (o = y, x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                       y++; if ((e -= x2) <= 0) { e += y2; rectFill(c, x, o, ++x, o = y); }
                    } rectFill(c, x, o, x + 1, y);
                 }
            else if (x2 >= (y2 = -y2)) {
                    for (o = x, y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                       x++; if ((e -= y2) < 0) { e += x2; rectFill(c, o, y, o = x, --y); }
                    } rectFill(c, o, y, x, y - 1);
                 } else {
                    for (o = y, x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                       y--; if ((e -= x2) <= 0) { e += y2; rectFill(c, x, o, ++x, o = y); }
                    } rectFill(c, x, o, x + 1, y);
                 }
       else if ((y2 -= y) == 0) rectFill(c, x, y, x + x2, y + 1);
            /* Note: the tests of sign of e MUST invert the condition where it's zero,
               otherwise some pixel positions will depend on the direction of drawing */
            else if (y2 > 0)
                 if ((x2 = -x2) >= y2) {
                    for (o = x, y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                       x--; if ((e -= y2) <= 0) { e += x2; rectFill(c, o, y, o = x, ++y); }
                    } rectFill(c, o, y, x, y + 1);
                 } else {
                    for (o = y, x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                       y++; if ((e -= x2) < 0) { e += y2; rectFill(c, x, o, --x, o = y); }
                    } rectFill(c, x, o, x - 1, y);
                 }
            else if ((x2 = -x2) >= (y2 = -y2)) {
                    for (o = x, y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {
                       x--; if ((e -= y2) < 0) { e += x2; rectFill(c, o, y, o = x, --y); }
                    } rectFill(c, o, y, x, y - 1);
                 } else {
                    for (o = y, x2 *= 2, y2 = (n = e = y2) * 2; n > 0; --n) {
                       y--; if ((e -= x2) < 0) { e += y2; rectFill(c, x, o, --x, o = y); }
                    } rectFill(c, x, o, x - 1, y);
                 }
   }

Note that in the previous version, as well as this one, the last point of the segment is not plotted. When you use successive lineTo() in the main function using that class, that final pixel will be plotted when the next segment will plot its first pixels.

When the polyline is closed by closePath(), it does not need to plot the last pixel because it will close automatically on the first pixel of the first drawn segment (so all pixels of the polygon will be effectively plotted); you may also use an additional lineTo() to join the first vertice, but it is not needed because the position of the first vertice is remembered.

If your polyline is not closed, you may use endPath() instead of closePath() to terminate the path and plot the last pixel (if you want it).

The polyline paths need to be open using moveTo() prior to any call to lineTo(), closePath(), or endPath(); otherwise all succeeding calls to lineTo() calls will be ignored up to next moveTo() call (this may be used to hide completely a polyline, by making the call to moveTo() conditional).

A fully inlined version (for those that are interested) follows, which includes the full optimisation (it also includes other comments):

Hidden Content:
   private void line(PaintDotNet.ColorBgra c, int x, int y, int x2, int y2) {
       int n, e, o;
       // Implementation note: we MUST NOT draw outside this bounding box
       // (otherwise random bands will be visible)
       int X1 = this.rectDraw.Left, X2 = this.rectDraw.Right,
           Y1 = this.rectDraw.Top,  Y2 = this.rectDraw.Bottom;
       PaintDotNet.Surface s = this.dstSurf;
       //  UL   -  U      UR
       //      \ 3 | 2 /
       //  -  4  \ | /  1
       //  L ------0------ R
       //     5  / | \  8  +
       //      / 6 | 7 \
       //  DL      D +    DR
       // When processing diagonals, e is the double offset from theoretical
       // line minus one, measured along the normal to the main progress axis,
       // multiplied by the length of segment projected on the main axis. If
       // two pixel candidates are at equal distance from the theoretical line,
       // always use the one with the highest normal coordinate for rounding it
       // up (so if e is zero, according tooctant, either increment this normal
       // coordinate with the slowest progression, or don't decrement it).
       if ((x2 -= x) == 0) { // moving vertically (D/0/U)
           if (X1 <= x && x < X2)
               if (y <= y2) { // moving vertically down (D/0)
                   if (y < Y2 && y2 > Y1) {
                       if (y2 > Y2) y2 = Y2; if (y < Y1) y = Y1;
                       while (y < y2) s[x, y++] = c;
                   }
               } else { // moving vertically up (U)
                   if (++y2 < Y2 && ++y > Y1) {
                       if (y > Y2) y = Y2; if (y2 < Y1) y2 = Y1;
                       while (y2 < y) s[x, y2++] = c;
                   }
               }
       } else if (x2 > 0) { // moving to the right (7/DR/8/R/1/UR/2)
           if ((y2 -= y) == 0) { // moving horizontally to the right(R)
               if (Y1 <= y && y < Y2 && x < X2 && (x2 += x) > X1) {
                   if (x2 > X2) x2 = X2; if (x < X1) x = X1;
                   while (x < x2) s[x++, y] = c;
               }
           } else if (y2 > 0) { // moving down to the right (7/DR/8)
               if (x2 >= y2) { // diagonal in the 8th octant (DR/8)
                   for (o = x, y2 <<= 1, x2 = (n = e = x2) << 1; n > 0; --n) {
                       x++; if ((e -= y2) <= 0) { // always increment y if equal distance
                           if (Y1 <= y && y < Y2 && o < X2 && x > X1) {
                               int p = (x <= X2) ? x : X2; if (o < X1) o = X1;
                               while (o < p) s[o++, y] = c;
                           } o = x; y++; e += x2; }
                   }       if (Y1 <= y && y < Y2 && o < X2 && x > X1) {
                               int p = (x <= X2) ? x : X2; if (o < X1) o = X1;
                               while (o < p) s[o++, y] = c;
                   }
               } else { // diagonal in the 7th octant (7)
                   for (o = y, x2 <<= 1, y2 = (n = e = y2) << 1; n > 0; --n) {
                       y++; if ((e -= x2) <= 0) { // always increment x if equal distance
                           if (X1 <= x && x < X2 && o < Y2 && y > Y1) {
                               int p = (y <= Y2) ? y : Y2; if (o < Y1) o = Y1;
                               while (o < p) s[x, o++] = c;
                           } o = y; x++; e += y2; }
                   }       if (X1 <= x && x < X2 && o < Y2 && y > Y1) {
                               int p = (y <= Y2) ? y : Y2; if (o < Y1) o = Y1;
                               while (o < p) s[x, o++] = c;
                   }
               }
           } else { // moving up to the right (1/UR/2)
               if (x2 >= (y2 = -y2)) { // diagonal in the 1st octant (UR/1)
                   for (o = x, y2 <<= 1, x2 = (n = e = x2) << 1; n > 0; --n) {
                       x++; if ((e -= y2) < 0) { // never decrement y if equal distance
                           if (Y1 <= y && y < Y2 && o < X2 && x > X1) {
                               int p = (x <= X2) ? x : X2; if (o < X1) o = X1;
                               while (o < p) s[o++, y] = c;
                           } o = x; y--; e += x2; }
                   }       if (Y1 <= y && y < Y2 && o < X2 && x > X1) {
                               int p = (x <= X2) ? x : X2; if (o < X1) o = X1;
                               while (o < p) s[o++, y] = c;
                   }
               } else { // diagonal in the 2nd octant (2)
                   for (o = y, x2 <<= 1, y2 = (n = e = y2) << 1; n > 0; --n) {
                       y--; if ((e -= x2) <= 0) { // always increment x if equal distance
                           if (X1 <= x && x < X2 && o >= Y1 && y < Y2) {
                               int p = (y >= Y1 - 1) ? y : Y1 - 1; if (o >= Y2) o = Y2 - 1;
                               while (o > p) s[x, o--] = c;
                           } o = y; x++; e += y2; }
                   }       if (X1 <= x && x < X2 && o >= Y1 && y < Y2) {
                               int p = (y >= Y1 - 1) ? y : Y1 - 1; if (o >= Y2) o = Y2 - 1;
                               while (o > p) s[x, o--] = c;
                   }
               }
           }
       } else { // moving to the left (3/UL/4/L/5/DL/6)
           if ((y2 -= y) == 0) { // moving horizontally to the left (L)
               if (Y1 <= y && y < Y2 && x > X1 && (x2 += x) < X2) {
                   if (x2 < X1) x2 = X1; if (x > X2) x = X2;
                   while (x > x2) s[x--, y] = c;
               }
           /* Below, the sign tests of e MUST invert the condition where it's zero,
              otherwise some pixel positions will depend on the direction of drawing. */
           } else if (y2 > 0) { // moving down to the left (5/DL/6)
               if ((x2 = -x2) >= y2) { // diagonal in the 5th octant (5/DL)
                   for (o = x, y2 <<= 1, x2 = (n = e = x2) << 1; n > 0; --n) {
                       x--; if ((e -= y2) <= 0) { // always increment y if equal distance
                           if (Y1 <= y && y < Y2 && o >= X1 && x < X2) {
                               int p = (x >= X1 - 1) ? x : X1 - 1; if (o >= X2) o = X2 - 1;
                               while (o > p) s[o--, y] = c;
                           } o = x; y++; e += x2; }
                   }       if (Y1 <= y && y < Y2 && o >= X1 && x < X2) {
                               int p = (x >= X1 - 1) ? x : X1 - 1; if (o >= X2) o = X2 - 1;
                               while (o > p) s[o--, y] = c;
                   }
               } else { // diagonal in the 6th octant (6)
                   for (o = y, x2 <<= 1, y2 = (n = e = y2) << 1; n > 0; --n) {
                       y++; if ((e -= x2) < 0) { // never decrement x if equal distance
                           if (X1 <= x && x < X2 && o < Y2 && y > Y1) {
                               int p = (y <= Y2) ? y : Y2; if (o < Y1) o = Y1;
                               while (o < p) s[x, o++] = c;
                           } o = y; x--; e += y2; }
                   }       if (X1 <= x && x < X2 && o < Y2 && y > Y1) {
                               int p = (y <= Y2) ? y : Y2; if (o < Y1) o = Y1;
                               while (o < p) s[x, o++] = c;
                   }
               }
           } else { // moving up to the left (3/UL/4)
               if ((x2 = -x2) >= (y2 = -y2)) { // diagonal in the 4th octant (UL/4)
                   for (o = x, y2 <<= 1, x2 = (n = e = x2) << 1; n > 0; --n) {
                       x--; if ((e -= y2) < 0) { // never decrement y if equal distance
                           if (Y1 <= y && y < Y2 && o >= X1 && x < X2) {
                               int p = (x >= X1 - 1) ? x : X1 - 1; if (o >= X2) o = X2 - 1;
                               while (o > p) s[o--, y] = c;
                           } o = x; y--; e += x2; }
                   }       if (Y1 <= y && y < Y2 && o >= X1 && x < X2) {
                               int p = (x >= X1 - 1) ? x : X1 - 1; if (o >= X2) o = X2 - 1;
                               while (o > p) s[o--, y] = c;
                   }
               } else { // diagonal in the 3rd octant (3)
                   for (o = y, x2 <<= 1, y2 = (n = e = y2) << 1; n > 0; --n) {
                       y--; if ((e -= x2) < 0) { // never decrement x if equal distance
                           if (X1 <= x && x < X2 && o >= Y1 && y < Y2) {
                               int p = (y >= Y1 - 1) ? y : Y1 - 1; if (o >= Y2) o = Y2 - 1;
                               while (o > p) s[x, o--] = c;
                           } o = y; x--; e += y2; }
                   }       if (X1 <= x && x < X2 && o >= Y1 && y < Y2) {
                               int p = (y >= Y1 - 1) ? y : Y1 - 1; if (o >= Y2) o = Y2 - 1;
                               while (o > p) s[x, o--] = c;
                   }
               }
           }
       }
   }

Posted
Holy bloody potato. Looooong tutorial. Thanks for all of your time you put into this. :shock:

I did not want to show a complete code, but a demonstration only of what can happen when programming some effects and ignoring the clippping rectangle (third parameter of the Render() routine).

This is a code to play with, because I've noted that various effects posted in these forums forget to text it, and are either slow, or show unexpected "banding" effects. And this also includes some of the basic examples shown with the documentation of CodeLab, which are needlessly slow (and also because I wonder if PaintDotNet can be improved to use hardware GPU pixel shaders to recompile the effects, or if CodeLab can automatically assert that concurrent threads will not be able to read from or write to surface areas controlled by each others).

Posted

I believe that this should be posted elsewhere. It is perhaps more suited to the Plugin Developers section and therefore...,

Posted
I believe that this should be posted elsewhere. It is perhaps more suited to the Plugin Developers section and therefore...,<Moved>

I am not sure that this had to go there, because it is basically part of tutorials and makes uses of an existing plugin (CodeLab). which is also described in the origin forum with other simple tutorials where I posted my message (which shows another aspect that will be useful even for the most basic custom effects built with CodeLab).

I did not use the beginner's tutorial forum however, and this was mostly related to the creation (according to the forum description which was about how to create some objects or effects).

What do you think of the suggested improvements for PaintDotNet or CodeLab? And why even the smallest rectangles are banded in multiple threads, even on single-core single-CPU systems? Is it to make sure that effect testers will effectively use the bounding rectangle correctly and see what bad effects can be produced if it is ignored?

Is there projects to include GPU support by converting effect renderers into hardware GPU pixel shaders? Does it require a new interface for CodeLab and others?

Posted

I find the following method much more conveinent for clipping to the current Rectangle of Interest (ROI):

// Author: BoltBait
// Name: Text
// Submenu: Render
// URL: http://www.boltbait.com/pdn/codelab/help/uielements.html
#region UICode
FontFamily Amount1 = new FontFamily("Arial"); // Font
int Amount2 = 12; // [8,72] Font Size
ColorBgra Amount3 = ColorBgra.FromBgr(0,0,0); // Font Color
Pair Amount4 = Pair.Create( 0.0 , 0.0 ); // Text Location
string Amount5 = "Paint.NET Rocks!"; // [0,255] Text
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
   Rectangle selection = EnvironmentParameters.GetSelection(src.Bounds).GetBoundsInt(); 
   int width = selection.Right - selection.Left;
   int height = selection.Bottom - selection.Top;
   int column = (int)Math.Round((float)((Amount4.First + 1.0) / 2.0) * width);
   int row = (int)Math.Round((float)((Amount4.Second + 1.0) / 2.0) * height);

   // Reset the destination canvas
   dst.CopySurface(src,rect.Location,rect);
   // Create a brush and graphics surface to write on
   SolidBrush Brush1=new SolidBrush(Color.FromArgb(Amount3.A,Amount3.R,Amount3.G,Amount3.);
   Graphics g = new RenderArgs(dst).Graphics;
   g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
   // Don't write outside of the current ROI
   g.Clip = new Region(rect);
   // Create a font from user selection
   Font SelectedFont=new Font(Amount1.Name, Amount2);
   // Write our text to the canvas
   g.DrawString(Amount5, SelectedFont, Brush1, column, row);
}

The benefit of this method is that is properly handles antialiased text EDIT: and lines.

More examples here: http://www.boltbait.com/pdn/codelab/hel ... ments.html

Hope this helps.

EDIT: The example given above is for text. However, I just noticed that you were talking about drawing lines.

Here is an example of using the same technique to draw lines: viewtopic.php?f=16&t=2580&p=16685#1 (Ignore everything in the first post, except the link to the VS2005 project source code.) This could be easily adapted to codelab by following my example above.

Posted
I find the following method much more conveinent for clipping to the current Rectangle of Interest (ROI):

More examples here: http://www.boltbait.com/pdn/codelab/hel ... ments.html

Hope this helps.

Note that using external clipping regions in the current graphics object will make your renderer much slower.

And it does not work when writing directly to the surface using the indexed array syntax. (Also I've found that the antialias feature of System.Drawing2D gives unreliable/unpredictable results which varies across versions and has various precision limits, even though it may hardware acceleration from the GPU if you have the correct display drivers, which do not have their own limitations). The number of bugs explodes when using Direct3D because of these display drivers that incorrectly compile your 2D/3D code into GPU instructions: the precision requirements are not always honored, and even those minimum requirements are not sufficient for some effects (so the effect will work on some systems, and not on some others.)

The most serious bugs found in hardware accelerations made by display drivers, is often in the geometry, on precise positioning of pixels and in the computed alpha values for antialiasing, and when computing splines and arcs (this can give spurious pixels on borders when polygons that are supposed to be perfectly jointive have their edges not perfectly matched or redrawn several times with alpha transparency enabled).

Antialiasing for text is also often inconsistant as it depends on user's settings of ClearType. For imagery applications, it is not very fun and can prohibit computing images on multiple heterogeneous systems, unless they use the same version of Windows, DirectX, display drivers and the same GPU hardware.

Posted

Here is an example of using the same technique to draw lines: viewtopic.php?f=16&t=2580&p=16685#1 (Ignore everything in the first post, except the link to the VS2005 project source code.) This could be easily adapted to codelab by following my example above.

This example on how to draw stars or regular thin polygons is extremely bogous (it does not respect the rectangles of interests and can produce banding effects due to multithreading) and is much slower than what with the Renderer class I gave above. Even the author admits these limitations, and he could rewrite it much more simply with this basic Renderer class.

Then the code that follows to draw a line is really slow as it uses floatting point instructions (and it does not render the lines the same way depending on directions or precision limits, and it also draws too many pixels, so some diagonal lines will have variable visual width). The Bresenham algorithm, for drawing any polynomial curves with small finite degrees, is know at least since the 1960's (and seeing that someone uses a floating point loop based on the line generic parametric equations {x=x(t), y=y(t)} that can be fully solved to eliminate the t variable completely, is extremely poor).

Posted
I believe that this should be posted elsewhere. It is perhaps more suited to the Plugin Developers section and therefore...,

I am not sure that this had to go there, because it is basically part of tutorials and makes uses of an existing plugin (CodeLab). which is also described in the origin forum with other simple tutorials where I posted my message (which shows another aspect that will be useful even for the most basic custom effects built with CodeLab).

What you're explaining is of more benefit to programmers than artists, this is why I moved it.

I left a shadow thread in place in the original location so it can also be found there. I hope this satisfies :D

Posted
Note that using external clipping regions in the current graphics object will make your renderer much slower.

I'd rather have good looking slow lines than ugly fast lines.

Plus, using GDI+ you can change the width of the line (something you can not do).

I think I'll stick to the other unless you can add antialising and variable line width.

Posted
Note that using external clipping regions in the current graphics object will make your renderer much slower.

I'd rather have good looking slow lines than ugly fast lines.

Lines computed with Bresenham algorithm are definitely NOT ugly (and even in your case, they are much better than your really ugly "thin line" implementation).

They are perfect up to the half pixel precision (and even lower if you use subpixel samping for antialiasing. I did not post here the antialising code, because computing it really fast requires much more advanced technics than just needlessly computing a larger image and downsampling it after (this would require lots of memory, and would be really slow).

Plus, using GDI+ you can change the width of the line (something you can not do).

I think I'll stick to the other unless you can add antialising.

Yes, but changing the width of the line (possibly with subpixel widths if antialising is enabled) requires another algorithm to fill polygons (whose geometry must first be computed). You currently use brushes to make thick lines, and using brushes is also really slow, as your plugin demonstrates it.

You can use the Bresenham algorithm to compute the perfect edges of polygons (in fact you'll have to compute and maintain a list of trapezoids and detect when a current edge needs to be terminated and when new edges need to be added in the active list of trapezoids.

The most difficult part is to compute the line caps when they are rounded: to make perfect fit, it requires complex maths and lots of tuning for the optimization. The simplest case occurs when line caps are squares, then the next simple case is with zero-miters, then with arbitrary miters (but there's an additonal requirement: you should respect the miter limit, i.e. their maximum length from the vertex position, relative to the line width).

More complex cases also occur with dashed lines, for which you must subdivide them in series of rectangles, and also for which the miters have to be dashed as well, creating non convex polygons... All this is required if you want to fully implement SVG graphics (supporting dashed lines or rounded caps is not required for SVG in the simplest rendering profile). Finally, you'll want to support fill brushes with patterns stored in external images (and make sure that they are synchronized: this is a lot of complex code to write, and without GPU support it will be really slow unless you use the Bresenham algorithm as well for subsampling or supersampling, if you want to avoid floatting points and still maintain perfect pixel accuracy or perfect subpixel alpha values).

This could not be posted here in this forum. But using GDI+ to draw thick lines is really slow, and your implementation for thin solid lines and without antialiasing is really ugly and slow too. In addition, it does not work as expected when your polyline color uses alpha transparency (the above code too: it does not really apply the alpha transparency and simply overrides the pixels, ignoring their previous content, but if it computed the transparency, some pixels would be covered multiple times, and the polyline could result in variable transparency, depending on the number of edges are passing through the same pixel).

A full implementation that draws line (including thin lines) correctly requires a polygon filler that computes the edges geometry, instead of just relying on the central position of vertice. That's something that I could not post here (it would be much too long and would no longer be a tutorial).

Posted
Lines computed with Bresenham algorithm are definitely NOT ugly (and even in your case, they are much better than your really ugly "thin line" implementation).

I agree that your line drawing code is much better than my thin line drawing code (MUCH faster).

But, my point is... there is not much call to draw aliased anything (lines or text). So, any useful code must include antialiased drawing. It doesn't matter how fast your code is, if no one's using it.

Posted
Lines computed with Bresenham algorithm are definitely NOT ugly (and even in your case, they are much better than your really ugly "thin line" implementation).

I agree that your line drawing code is much better than my thin line drawing code (MUCH faster).

But, my point is... there is not much call to draw aliased anything (lines or text). So, any useful code must include antialiased drawing. It doesn't matter how fast your code is, if no one's using it.

May be I'll post a modified code (simplified a lot) to render antialiased thin lines (using a wide surface for each 1-pixel-high scan line), but I definitely cannot post here a complete code which computes the geometry of filled polygons, or that optimizes the rendering time by subsampling only the pixels that really need to be subsampled.

Posted

"Your code is ugly, slow, and generally sucky."

"No, your code is ugly, slow, and makes puppies sad."

Shut up. Both of you.

@verdy_p: BoltBait (and most of the other plugin authors around here including myself) writes plugins for Paint.NET for fun, and the benefit of the Paint.NET community. Very few of us see a single penny for our work. Being so completely anal about every little detail -- "if you do this, this pixel may get written more than once! the end of the world is nigh!" -- sure as hell takes all the fun out of it, and would make every plugin take so long to release that nobody would ever get to use it.

Excuse me while I go change my major to CompSci so my next plugin can be algorithmically perfect in every way. I hope nobody was expecting to see a new plugin from me for the next 4 years or so.

If you want your plugins to be perfect, correct, and fast, feel free; nobody's stopping you. And constructive criticism is certainly welcome. But bashing others' decision to give away their imperfect work for free is not going to make you any friends around here.

xZYt6wl.png

ambigram signature by Kemaru

[i write plugins and stuff]

If you like a post, upvote it!

Posted

pyrochild, that's a bit strong.

My main point that I was trying to make (and I think I finally made it in my previous post) is that an algorithm is only as good as the use people get out of it. My issue was not that the code was bad, in fact the code he posted is quite good. My issue was that without anitaliasing and variable line width, no one's going to use it. He could spend the rest of his life optimizing that code to the point that it is the fastest line drawing code in the world... but it would just sit in a book somewhere because no one could find a use for it.

I presented a different way to draw antialiased text and lines, and it is slower than his code. But, it is something that people can get some use out of. And, to me, that's more important than the few extra milliseconds I'll have to wait for my line to draw.

Posted

@verdy_p:

When multiplying or dividing by 2^n you can use shift left or shift right.

The following code snippet:

for (y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {

becomes:

for (y2 <<= 1, x2 = (n = e = x2) << 1; n > 0; --n) {

It shaves off more than a few CPU cycles and doesn't use the FPU.

Posted
"Your code is ugly, slow, and generally sucky."

"No, your code is ugly, slow, and makes puppies sad."

Shut up. Both of you.

Neither I, nor BoltBait, used all these words. So "shut up" yourself too (why did you need to insult both of us?).

I just wanted to show that there's improvement possible in the sample code posted by BoldBait, and he admits that. I have definitely not bashed him. (and in fact such code has another use in my own implementation, that computes many more complex geometries with really lots of lines and curves in the same scene). In the background, at the lowest implementation level where most of the CPU time is spent in an internal loop, you need such extra optimization (which will be beneficial as well when implementing antialising on top of the basic renderer: this is why it is written in a separate class for easier reusability), but of course you won't need it to draw a simple shape.

He says that the code will not be useful without antialising. But antialising renderers are always built on top of a non-antialising renderer, that does not necessarily need any modification in itself, because you can build another class using it. So the non-antiliasing renderer should be really fast in order to build other bricks on top of it.

And antialising was not the subject of this thread. The code was posted in order to demonstrate something else, with enough facilities to allow easy modifications.

Posted
@verdy_p:

When multiplying or dividing by 2^n you can use shift left or shift right.

The following code snippet:

for (y2 *= 2, x2 = (n = e = x2) * 2; n > 0; --n) {

becomes:

for (y2 <<= 1, x2 = (n = e = x2) << 1; n > 0; --n) {

It shaves off more than a few CPU cycles and doesn't use the FPU.

Wrong! this code coes not use the FPU, everything is integers only, the multiplications and divisions is between integers and returns integers only, and the dotNet compiler already optimizes integer divisions or multiplications by constant powers of 2 using binary shifts. You won't gain any CPU cycle here. So I used simple multiplications just to avoid obscuring the code in this basic tutorial, when there's absolutely no benefit here. This is DotNet code, not a non-optimizing basic C compiler.

Posted

>the dotNet compiler already optimizes integer divisions or multiplications by constant powers of 2 using binary shifts.

@verdy_p

Thanks for enlightening me. I didn't know that. (The principle is still sound though.)

What we need is higher resolutions VDUs, say 600dpi, then we wouldn't need antialiasing at all.

Posted
pyrochild, that's a bit strong.

Probably, but my point still stands. The argument was pointless. Clearly, you're in the pragmatic "shipping is a feature" camp, and OP is in the idealistic "my code is perfect, yours should be, too" camp. They both have merit, but arguing about them is stupid.

"Your code is ugly, slow, and generally sucky."

"No, your code is ugly, slow, and makes puppies sad."

Shut up. Both of you.

Neither I, nor BoltBait, used all these words.

Please look up sarcasm and paraphrasing.

(why did you need to insult both of us?)

Because you were both being ridiculous. Would you rather I'd only insulted you?

xZYt6wl.png

ambigram signature by Kemaru

[i write plugins and stuff]

If you like a post, upvote it!

Posted

hey settle down there boys. put the thing in a dll form and I'll try to work with it in making images. That's what it all comes down to. Give it to the ones that will make the decision on how well it translates into common useage by the common paint.net user.

super fast doesn't always mean super plugin (from an artist point of view)

from all the posts I've read them several times I still can't figure out what in the world this is supposed to do. zooom over my head.

ciao OMA

Posted
hey settle down there boys. put the thing in a dll form and I'll try to work with it in making images. That's what it all comes down to. Give it to the ones that will make the decision on how well it translates into common useage by the common paint.net user.

super fast doesn't always mean super plugin (from an artist point of view)

I still can't figure out what in the world this is supposed to do. zooom over my head.

This is not the subject of this thread. This is a basic tutorial explaining some examples about what can be done when creating an extension. By itself it is not an extension as it contains no control for parameters. This is code to play with, that you can compile immediately yourself using the CodeLab plugin (which also does nothing by itself: you have to write your code).

Almost everything in this forum contains no plugin: you're in the wrong forum for that. If you don't know what is CodeLab or what is the PaintDotNet API, look elsewhere. There's absolutely nothing complicate here to understand what it is about.

And yes, I need "superfast" renderers (and you also need it too when you have to build complex images using many filters that can combine each other). But once again, restart from the initial message : it does not speak really about performance, but about "rectangles of interest" and why it is important to not draw outside of it : many bugs in effects are caused by lack of understanding of why they are important, and it shows what they are, then gives a code that can work correctly as expected, but that will not work with some minor modifications explained but that you could incorrectly assume.

Then it proposes a basic (but generic) Renderer class, that can be used to build shapes (using self-explained methods similar to the terminology used in SVG: you certainly know what is SVG, but you can't do anything with it without "programming" the shapes you want into a XML script (so you'll write the SVG code manually, or you'll use an visual editor to build compelx things like geographic maps).

But speed is not the only subject of this class: you also need precision in the result. This code is precise up to the half-pixel and tested in all the particular cases (within the range limits of 32-bit integers, much more than what you need in an actual image).

It is followed by an example which is very simple but can be used to test it fully. All this is supposed to be compiled and experimented immediately from the CodeLab plugin where you get the effect running instantly with the modifications you make in its editor. You don't need a separate DLL, and you don't even need a complete development kit. CodeLab is enough to compile this into a working DLL (but you also don't need to build this as a permanent DLL : this is just code that you can play with).

(And I don't like distributing executable DLLs, which are potentially unsafe; I prefer sources for security, unless the author is clearly identifiable and does not want to be known as a malware author.) In fact almost all free effects should be distributed as CodeLab source scripts, and can be posted here as sources instead of DLLs hosted on random sites: I won't download and use almost all these discussed (possibly bogous and malicious) DLLs.

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...