Jump to content

Box Outlining


Recommended Posts

I could use a little help on this one ;)

 

I'm trying to recreate an outlining effect I saw on Watabou's one-page dungeon generator. I want to surround an object with little gray boxes something like this image.

 

box-outlining.png

I had a very rough stab at it. Basically my plan was to step through the image and test the transparency of the target pixel. If it was transparent, throw a little box around it. The code I wrote trying to hack this out was truly awful.

 

Spoiler

 

/* ========================================================================== */
/*                                                                            */
/*   BoxOutline.cs                                                            */
/*   (c) 2024 Ego Eram Reputo                                                 */
/*                                                                            */
/*   Description: surrounds a selection with shaded boxes                     */
/*                                                                            */
/* ========================================================================== */

// Name: BoxOutline
// Author: Ego Eram Reputo
// Submenu: Render
// URL: http://www.getpaint.net/redirect/plugins.html

#region UICode
IntSliderControl Amount2 = 25; // [15,100] Box Size
ReseedButtonControl Amount1 = 0; // Reseed
IntSliderControl Amount3 = 5; // [1,25] Spacing
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
    Rectangle selection = EnvironmentParameters.SelectionBounds;
    int boxSize = Amount2/2;
    int Spacing = Amount3;
    int rndHeight, rndWidth, m,p;
    ColorBgra PrimaryColor = (ColorBgra)EnvironmentParameters.PrimaryColor;

    Graphics g = new RenderArgs(dst).Graphics;
    g.Clip = new Region(rect);
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    Pen outlinePen = new Pen(PrimaryColor, 2);
    SolidBrush solidBrush = new SolidBrush(Color.Beige);

    dst.CopySurface(src,rect.Location,rect);

    for (int y = selection.Top; y < selection.Bottom; y += boxSize+Spacing)
    {
        for (int x = selection.Left; x < selection.Right; x += boxSize+Spacing)
        {
            if (src[x,y].A >0 )
            {
                rndHeight = RandomNumber.Next( boxSize/2.0 )+RandomNumber.Next(boxSize/2);
                rndWidth = RandomNumber.Next(boxSize/2)+RandomNumber.Next(boxSize/2);
                
                m=x-rndWidth-boxSize/2+1;
                if (m<0) {m=0;}
                p=y-rndHeight-boxSize/2+4;
                if (p<0) {p=0;}

                g.FillRectangle(solidBrush, m-rndWidth, p-rndHeight, boxSize*2+rndWidth, boxSize*2+rndHeight);
                g.DrawRectangle(outlinePen, m-rndWidth, p-rndHeight, boxSize*2+rndWidth, boxSize*2+rndHeight);
            }
        }
    }
}

 

 

Results were ......promising!? (at least it was working right 😁)

 

boxoutline2.png

 

Does anyone want to help me write something a little more elegant?

  • You're a Smart Cookie! 2
Link to comment
Share on other sites

One observation regarding the algorithm...


Comparing your results with those from the one-page dungeon generator (OPDG), it appears that you are creating each box with a random width and height.

I think the OPDG uses a fixed set of box sizes and the algorithm is along the lines of:

 

Walk around the border and:

1. Randomly choose a box size from the set

2. Randomly choose the amount of overlap for this box (within certain constraints) with the box previously placed.

  • Like 1
Link to comment
Share on other sites

Looks like a fun thing to try out. I won't do it in C# though. And I even have more idea than simply orthogonal directions.

G'MIC Filter Developer

 

I am away from this forum for undetermined amount of time: If you really need anything related to my PDN plugin or my G'MIC filter within G'MIC plugin, then you can contact me via Paint.NET discord, and mention me.

Link to comment
Share on other sites

Here is a classic script to help you determine when you are on the edge of a selection...

 

Spoiler
// Name:
// Submenu:
// Author:
// Title:
// Version:
// Desc:
// Keywords:
// URL:
// Help:
#region UICode
ReseedButtonControl Amount1 = 0; // Randomize
#endregion

// Here is the main multi-threaded render function
// The dst canvas is broken up into rectangles and
// your job is to write to each pixel of that rectangle
void Render(Surface dst, Surface src, Rectangle rect)
{
    // uint seed = RandomNumber.InitializeSeed(RandomNumberRenderSeed, rect.Location);
    // Delete this line if you don't need the selection outline shape
    PdnRegion selectionRegion = EnvironmentParameters.GetSelectionAsPdnRegion();


    // Step through each row of the current rectangle
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        // Step through each pixel on the current row of the rectangle
        for (int x = rect.Left; x < rect.Right; x++)
        {
            ColorBgra SrcPixel = src[x,y];
            ColorBgra DstPixel = dst[x,y];

            ColorBgra CurrentPixel = SrcPixel;

            if ( (!selectionRegion.IsVisible(x-1,y)) || (!selectionRegion.IsVisible(x,y-1)) || (!selectionRegion.IsVisible(x+1,y)) || (!selectionRegion.IsVisible(x,y+1)) )
            {
                // This pixel is next to the marching ants

                // as an example, I'm changing that pixel to a random color
                CurrentPixel = ColorBgra.FromBgr((byte)RandomNumber.Next(ref RandomNumberRenderSeed,255),(byte)RandomNumber.Next(ref RandomNumberRenderSeed,255),(byte)RandomNumber.Next(ref RandomNumberRenderSeed,255));


            }
            else
            {
                // This pixel is NOT next to the marching ants
            }
            dst[x,y] = CurrentPixel;
        }
    }
}

 

 

So, select your building outline and run the script.

 

Hope this helps.

  • You're a Smart Cookie! 1
Link to comment
Share on other sites

Spoiler
 using (Graphics graf = new RenderArgs(dest).Graphics)
                            {
                                graf.Clip = new Region(rect);
                                graf.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
                                using (GraphicsPath botface = new GraphicsPath())
                                {
                                    botface.StartFigure(); botface.AddLines(basecorners); botface.CloseFigure();
                                    using (SolidBrush solidtestbrush = new SolidBrush(bfcolt)) { graf.FillPath(solidtestbrush, botface); graf.DrawPath(new Pen(bfcol), botface); }
                                }//end botface block
// another five faces done here in the real code
}//end graf block

// This is one of the many rectangles drawn in my Cuboids code (done in Visual studio not codelab). 'botface' means bottom face of a cube (not an insult;-)
// Note:'dest' is surface which gets copied to 'dst' you'll prbably write straight to dst after copying src to dst.
// I place Graphics paths, Brushes and Pens in 'using' statement blocks so they get disposed when no longer needed.
// Otherwise you could run out of memory if you forget to seperate Dispose statements for everything created.
// basecorners is an array of 4 PointFs calculated before creating the graphics stuff.

 

Hope that helps... I've found it better to use 'using' statements to ensure graphics objects get disposed of.

  • You're a Smart Cookie! 1

 

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

 

PdnForumSig2.jpg

Link to comment
Share on other sites

FWIW, the classic effect system is getting deprecated, and soon -- I strongly encourage you to switch to BitmapEffect or GpuEffect (or probably [PropertyBased]GpuDrawingEffect in this case). It's worth learning this now before you end up with effect code that you can no longer maintain (compile) when newer versions of Paint.NET are released. This deprecation is starting with v5.1, where the classic effect system will be marked with [Obsolete("...", false)] (compiler warning to not use it), and then in the next release (e.g. v5.2 or v6.0) it'll be [Obsolete("...", true)] (compiler error preventing you from using it).

 

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

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

21 hours ago, Tactilis said:

I think the OPDG uses a fixed set of box sizes

Hadn't considered that. I'll have a look and see if I can make six or seven boxes work.

 

16 hours ago, BoltBait said:

Here is a classic script to help you determine when you are on the edge of a selection...

Nice! Thank you @BoltBait

 

14 hours ago, Red ochre said:

Hope that helps... I've found it better to use 'using' statements to ensure graphics objects get disposed of.

Thanks Red.

 

12 hours ago, Rick Brewster said:

FWIW, the classic effect system is getting deprecated, and soon -- I strongly encourage you to switch to BitmapEffect or GpuEffect (or probably [PropertyBased]GpuDrawingEffect in this case).

Quite right. I will change of course. The posted code was something from a year or so ago (I just had another play with it to remember where I got to).

  • Upvote 1
Link to comment
Share on other sites

Some progress.......

 

// 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 = 35; // [15,100] Size
IntSliderControl Amount3 = 9; // [2,25] Spacing
#endregion

protected override unsafe void OnDraw(IDeviceContext deviceContext)
{
    deviceContext.DrawImage(Environment.SourceImage);  // preserve background

    // find out where our selection is located
    RectInt32 selection = Environment.Selection.RenderBounds;

    // variables
    int boxSize = Amount2/2;
    int Spacing = Amount3+1;
    int doubleSpacing = Amount3*2;
    int thickness = 2;
    int rndHeight, rndWidth; 
    int step = boxSize*7/10;

    // define your brush and stroke style
    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

    Random rnd = new Random();
        for (int y = selection.Top; y < selection.Bottom; y += step)
    {
        for (int x = selection.Left; x < selection.Right; x += step)
        {

           //if source pixel is opaque )
           
            rndWidth = rnd.Next(Spacing, doubleSpacing);
            rndHeight = rnd.Next(Spacing, doubleSpacing);

            deviceContext.FillRectangle(x-rndWidth, y-rndHeight, boxSize, boxSize, fillBrush);    
            deviceContext.DrawRectangle(x-rndWidth, y-rndHeight, boxSize, boxSize, outlineBrush, thickness, boxStrokeStyle);    
        }
    }
}

 

I haven't figured out how to poll the source pixel yet, so I'm just filling selections at the moment. Still, I think looks great! :mrgreen:

 

boxoutliningdemo-GPU.png

 

  • Thanks 1
  • Upvote 2
  • You're a Smart Cookie! 1
Link to comment
Share on other sites

10 hours ago, Ego Eram Reputo said:

I haven't figured out how to poll the source pixel yet

 

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.

 

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.

  • 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

  • 1 month later...

Time to admit I'm lost.

 

    // use Environment.GetSourceBitmapBgra32(), which will give you an IEffectInputBitmap<ColorBgra32>
    IEffectInputBitmap 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> outputRegion = BitmapLockExtensions.AsRegionPtr<ColorBgra32>(myLock);

 

The error I'm getting in the line which calls Lock()

 

Quote

Error at line 50: Cannot implicitly convert type 'PaintDotNet.Imaging.IBitmapLock' to 'PaintDotNet.Imaging.IBitmapLock<PaintDotNet.Imaging.ColorBgra32>'. An explicit conversion exists (are you missing a cast?) (CS0266)

 

Link to comment
Share on other sites

// use Environment.GetSourceBitmapBgra32(), which will give you an IEffectInputBitmap<ColorBgra32>
IEffectInputBitmap sourceImage = Environment.GetSourceBitmapBgra32();

You need to use IEffectInputBitmap<ColorBgra32>. Then you'll have the version of Lock which returns IBitmapLock<ColorBgra32>

 

GetSourceBitmapBgra32() is an extension method that calls GetSourceBitmap<ColorBgra32>(), which itself is an extension method which will call GetSourceBitmap(PixelFormats.Bgra32) and then do the appropriate casting from IEffectInputBitmap to IEffectInputBitmap<ColorBgra32>. 

 

// Then use AsRegionPtr() to get it as a RegionPtr<ColorBgra32> 
RegionPtr<ColorBgra32> outputRegion = BitmapLockExtensions.AsRegionPtr<ColorBgra32>(myLock);

You should call the extension method as if it's a regular method on the object, e.g. myLock.AsRegionPtr(). Once you fix the GetSourceBitmap stuff above, this will work correctly.

  • 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

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