Sign in to follow this  
maccas

Add depth plug-in

Recommended Posts

Hi all,

I just picked up Paint.NET the other day after someone at work gave me a recommendation. I've been looking to try use it to create some icons and other computer graphics (as opposed to photo editing).

The issue I've come across is trying to add (the appearance of) depth to a 2D shape. I had a look at some of the tutorials to figure out how to do this and they seem to suggest either using:

  • motion blur, which works but is a bit of a hack and you have to process the thing after to get the correct effect, or
  • duplicating, shifting and merging layers repeatedly, which also works but only on the axes or diagonal and is a little laborious

So I thought in for penny, in for a pound and had a crack at writing my first plug-in to automate the job. I apologise if someone's already done this as I'm new to PdN.

The plug-in is designed to work on an image on transparent background i.e. it doesn't detect white or any other colour as background and in particular if you pass it an image with no transparency there will be no effect! There are three parameters:

  • Depth - how deep the offset fill is calculated at. The higher you put this value the slower the effect is to calculate as the code needs to check along the depth length for every output pixel (I need to have a ponder on whether this is optimal)
  • Angle - the angle at which the shape is filled back at
  • High Quality Mode - you'll get marginally better performance if you uncheck the box but the outer edges of the fill shape are more realistic if this mode is switched on. I put this in so the image was more responsive when you were playing around with angle and depth but to be honest I can't detect much difference in performance on my PC

I find it most useful if you duplicate the original surface layer so that after you're done you've got a surface layer and a depth layer play around with separately. In the screenshots below I've done the following:

  1. got a bit of text
  2. rotated it (Layers -> Rotate / Zoom ...)
  3. duplicated the layer
  4. recoloured the top layer yellow
  5. used Add Depth on the bottom layer

post-84905-131368465711_thumb.png

post-84905-13136846769_thumb.png

Here's the DLL: Add Depth v1.0.zip

Not sure if this bit's for the code development forum but I attach the CodeLab source code below. I'd appreciate it if anyone can point out how to make the thing more efficient or if I've got any basic errors going on. Pointers on better coding generally are welcomed as this is the first time I've tried out C++.

// Title: Add Depth
// Author: maccas
// Submenu: Render
// Name: Add Depth
// URL: http://www.getpaint.net/redirect/plugins.html
#region UICode
int Amount1 = 10; // [1,100] Depth
double Amount2 = 45; // [-180,180] Angle
bool Amount3 = false; // [0,1] High Quality Mode
#endregion

// Global variables to hold the selection bounds (assumes a single rectangle is selected)
// Saves having to call GetSelection().GetBoundsInt() with each call to GetTransPixel()
private int maxX, maxY, minX, minY;

// Routine to ensure intergrity of integer to byte conversion
private byte Clamp2Byte(int iValue)
{
   if (iValue < 0) return 0;
   if (iValue > 255) return 255;
   return (byte)iValue;
}

// Routine to ensure intergrity of double to byte conversion
private byte Clamp2Byte(double dValue)
{
   if (dValue < 0) return 0;
   if (dValue > 255) return 255;
   return (byte)Math.Round(dValue);
}

// Routine returns the translated pixel that is 'depth' away  from [x,y] at an angle of 'angle' 
private ColorBgra GetTransPixel (Surface src, int x, int y, int depth, double angle)
{
   // Declare initial variables
   double deltaX, deltaY;

   // Get the offset amounts
   // Reverse x offset as [0,0] is top left of canvas but 0 assumed to point horizontal right
   deltaX = -(double)depth * Math.Cos(angle);
   deltaY = (double)depth * Math.Sin(angle);

   // If the offset puts us outside the bounds of the selection then quit out
   if ((x + deltaX > maxX - 1) || (x + deltaX < minX + 1) || 
       (y + deltaY > maxY - 1) || (y + deltaY < minY + 1))
   {
       // Create pixel with hex colour #FFFFFF and full transparancy
       return ColorBgra.FromBgra((byte)255,(byte)255,(byte)255,(byte)0);
   }

   // Declare more variables as we're in range to do something
   int iDeltaX, iDeltaY;
   ColorBgra srcPixel1;

   // Round down the offsets plus 0.5 to get the principal offset pixel
   // using the 0.5 as we're measuring to the mid-point of pixels
   iDeltaX =  (int)Math.Floor(deltaX + 0.5);
   iDeltaY =  (int)Math.Floor(deltaY + 0.5);

   // Get the pricipal offset pixel
   srcPixel1 = src[x+iDeltaX,y+iDeltaY];

   // Only mix the 4 pixels if we're working in high quality mode
   if (!Amount3) return srcPixel1;

   // Declare last batch of variables for the full monty calculation
   ColorBgra srcPixel2, srcPixel3, srcPixel4;
   double mixX, mixY;
   bool xUp;
   byte outR, outG, outB, outA;

   // Get the x offset pixel
   if ((deltaX - iDeltaX) > 0)
   {
       srcPixel2 = src[x+iDeltaX+1,y+iDeltaY];
       mixX = 1 - (deltaX - iDeltaX);
       xUp = true;
   }
   else
   {
       srcPixel2 = src[x+iDeltaX-1,y+iDeltaY];
       mixX = 1 + (deltaX - iDeltaX);
       xUp = false;
   }

   // Get the y and the xy offset pixel
   if ((deltaY - iDeltaY) > 0)
   {
       srcPixel3 = src[x+iDeltaX,y+iDeltaY+1];
       mixY = 1 - (deltaY - iDeltaY);
       if (xUp)
       {
           srcPixel4 = src[x+iDeltaX+1,y+iDeltaY+1];
       }
       else
       {
           srcPixel4 = src[x+iDeltaX-1,y+iDeltaY+1];
       }
   }
   else
   {
       srcPixel3 = src[x+iDeltaX,y+iDeltaY-1];
       mixY = 1 + (deltaY - iDeltaY);
       if (xUp)
       {
           srcPixel4 = src[x+iDeltaX+1,y+iDeltaY-1];
       }
       else
       {
           srcPixel4 = src[x+iDeltaX-1,y+iDeltaY-1];
       }
   }

   // Compute the output pixel as the mix of the four source pixels
   outB = Clamp2Byte(mixX*mixY*(double)srcPixel1.B + 
       (1-mixX)*mixY*(double)srcPixel2.B + 
       mixX*(1-mixY)*(double)srcPixel3.B + 
       (1-mixX)*(1-mixY)*(double)srcPixel4.;

   outG = Clamp2Byte(mixX*mixY*(double)srcPixel1.G + 
       (1-mixX)*mixY*(double)srcPixel2.G + 
       mixX*(1-mixY)*(double)srcPixel3.G + 
       (1-mixX)*(1-mixY)*(double)srcPixel4.G);

   outR = Clamp2Byte(mixX*mixY*(double)srcPixel1.R + 
       (1-mixX)*mixY*(double)srcPixel2.R + 
       mixX*(1-mixY)*(double)srcPixel3.R + 
       (1-mixX)*(1-mixY)*(double)srcPixel4.R);

   outA = Clamp2Byte(mixX*mixY*(double)srcPixel1.A + 
       (1-mixX)*mixY*(double)srcPixel2.A + 
       mixX*(1-mixY)*(double)srcPixel3.A + 
       (1-mixX)*(1-mixY)*(double)srcPixel4.A);

   // Make the Pixel and return the output
   return ColorBgra.FromBgra(outB,outG,outR,outA);
}

// Main routine
void Render(Surface dst, Surface src, Rectangle rect)
{
   // Record the dimensions of the environment to the global variables
   Rectangle selection = EnvironmentParameters.GetSelection(src.Bounds).GetBoundsInt();
   minX = selection.Left;
   maxX = selection.Right;
   minY = selection.Top;
   maxY = selection.Bottom;

   // Convert degrees to radians
   double rad = 2*Math.PI*(Amount2/360);

   // Declare some variables we're going to need
   ColorBgra pixel; 
   byte[] outputs = new byte[Amount1];
   byte maxAlpha;

   for (int y = rect.Top; y < rect.Bottom; y++)
   {
       for (int x = rect.Left; x < rect.Right; x++)
       {
           // Clear the output array & max alpha counter
           for (int d = 0; d < Amount1; d++) outputs[d] = 0;
           maxAlpha = 0;

           // Initialse pixel variable to stop compiler complaining when setting dst[x,y]
           // Not sure why I need to do this!
           pixel = ColorBgra.FromBgra((byte)255,(byte)255,(byte)255,(byte)0);

           // Loop through the offset distances from 1 to depth and 
           // record the alpha of each translated pixel
           // stop if we get a fully opaque pixel
           for (int d = 0; d < Amount1; d++)
           {
               // Get the state of the translated pixel at depth d
               pixel = GetTransPixel (src, x, y, d, rad);
               outputs[d] = (byte)pixel.A;

               // Record the max alpha value and quit out if fully opaque
               if (outputs[d] > maxAlpha) maxAlpha = outputs[d];
               if (maxAlpha == 255) break;

           }

           // Find the first pixel at Max Alpha
           // No point in running GetTransPixel() again if alpha is at max 
           // as pixel variable already holds correct reference
           if (maxAlpha != 255)
           {
               for (int d = 0; d < Amount1; d++)
               {
                   if (outputs[d] == maxAlpha)
                   {
                       pixel = GetTransPixel (src, x, y, d, rad);
                       d = Amount1;
                   }
               }
           }

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

I also thought it might be useful to get hold of the the bottom surface as a separate layer, which I've coded up as a different plug-in as Code Lab scripts only seem to be able to work within one layer. It's called Offset Shadow and is in the zip file above. In and of itself it's not much to write home about as you'd just duplicate and shift a layer normally but if you use the same settings as the Add Depth plug-in you get the pixel perfect bottom layer for good measure.

Enjoy!

Maccas

Share this post


Link to post
Share on other sites

It works nicely! thanks for sharing and thanks for publishing the source too (and for explaining things so well in the comments).

It is slightly similar to 'trail', but still very useful and effective. Many Thanks;)

Here's my 'test' using it :

depthplugintest.jpg

Edited by Red ochre

Share this post


Link to post
Share on other sites

Hey Maccas,

Great first plugin! I really like the way you've created this. Nice icon and a good explanation of how the effect works & how to use it.

diagona.032.pngdiagona.032.png

[code// Routine to ensure intergrity of integer to byte conversion
private byte Clamp2Byte(int iValue)
{
if (iValue < 0) return 0;
if (iValue > 255) return 255;
return (byte)iValue;
}

// Routine to ensure intergrity of double to byte conversion
private byte Clamp2Byte(double dValue)
{
if (dValue < 0) return 0;
if (dValue > 255) return 255;
return (byte)Math.Round(dValue);
}
[/code]

Both these routines can be replaced with a call to Int32Util.ClampToByte e.g.

This:

outB = Clamp2Byte(mixX*mixY*([color="#0000ff"]double[/color])srcPixel1.B + 

Becomes:

outB = Int32Util.ClampToByte(mixX*mixY*([color="#0000ff"]double[/color])srcPixel1.B + 

You're right that plugins can only access the active layer. This is not just a limitation of Codelab.

Share this post


Link to post
Share on other sites

Just as I feared ... someone's already done this only way better!

Thanks for all your encouragement and the coding pointers. Would it be it useful to add perspective to this plug-in for extra 3D goodness (& to differentiate from Pyrochild's more comprehensive offering)?

Share this post


Link to post
Share on other sites

It is alright I like the simple UI.

I was thinking if it is possible to combine the rotate/zoom tool to generate a 3D spiral effect, shapes like this, and it would be wonderful if the center of rotation be movable in pixels too, and also zoom in or out for each layer.

This is not a request of a plugin I am just throwing an idea, and I am not sure if is doable because I am zero at coding.

element.jpg3Drotate.jpg

Share this post


Link to post
Share on other sites

Would it be it useful to add perspective to this plug-in for extra 3D goodness (& to differentiate from Pyrochild's more comprehensive offering)?

Yes please!

It strikes me that you have done all the hard work in the code already, and that the DeltaX & Y values could be calculated as the ratio of the distance to a 'vanishing' point.

I think 'zoom blur' does something similar but only 'smears' away from the central point, it would be more versatile if smearing towards the point was included.

Also if the position of the vanishing point was allowed to be outside the canvas area (by using integer X,Y,sliders instead of the double vector UI), this would allow more subtle perspective angles.

- Or perhaps just a single 'multiply distance' slider set at default of 1, applied to the standard double vector UI?

Anyway, I know these things are much easier said than done, (but your code looks much more competent than mine!), so good luck, this could be a really useful tool.

Thanks again for sharing.

Share this post


Link to post
Share on other sites

@yellowman: the effect you show there would be super difficult to achieve through a plug-in (not impossible mind!). Essentially your original surface now has the appearance of no longer being flat but is curved and twisted in all sorts of weird and wonderful ways, one section near the top even has both opposing edges showing depth at the same point. Just coding the UI capture what distortions you wanted you be a bit of a challenge.

@Red ochre: This is more do-able but on thinking about it, it is not particularly simple either (happy to be proved wrong). I like a challenge so I'll have a crack at it. I don't have loads of spare time so it might take more than a few days.

Here's my thoughts on the matter thus far, feel free to chip in if you've got any bright ideas on how to tackle the problem and/or any useful source code:

  • my code doesn't do most of it at all as I only need worry about combining 4 source pixels to get any one destination pixel but perspective, with it's shrinking of the receding planes, could put many source pixels to any one destination pixel. Not the end of the world but will require a different approach
  • also I get to assume constant angle of translation in my current code but it will vary in any perspective enabled routine
  • thinking about the varying angle stuff I'm coming round to the fact that a realistic depth effect is actually intimately tied to the original layer rotation manipulation (i.e. step 2 of my original "how to use..."). The rotation actually constraints where is it reasonable to put the vanishing points so I'm thinking that any 3D perspective effect should include the rotation as well so that it knows how the planes have moved relative to the observer.
  • Any starting image should be assumed to be 2D and face-on to the observer with vanishing points in the middle of the page, to right and left infinity and to top and bottom infinity. That middle of the page point is the sole visible vanishing point of the two front and back vanishing points, which should also be at infinity: you can't see the back point because it's behind you. Since our effect is setting out to cast a flat 2D object directly backwards in space tracking the position of these 6 vanishing points is all (!) we need to do.
  • Our effect therefore needs to allow us to specify:
    • depth of cast into 3D (obviously)
    • rotation of the original layer (as discussed above)
    • ability to bring vanishing points in from infinity to some finite value, you don't get any perspective without this
    • ... and getting inspiration from Pyrochild's work, the ability to fade out or blur the image at greater depth

    [*]I think the UI coding alone takes me slightly beyond the bounds of what's sensibly do-able in Codelab so it looks like I'll also need to migrate to VS. If anyone has any good tutorials / guides on how someone can do this from scratch (i.e. what programs do I need to install onwards, I'm currently a total beginner) I'd be most obliged otherwise I'll just have to figure it out as I go along.

Last point: it feels like this might no longer a post about a published plug-in but a development discussion thread. If the post needs splitting or moving can someone please advise as I don't want to break any etiquette.

Share this post


Link to post
Share on other sites
@yellowman: the effect you show there would be super difficult to achieve through a plug-in (not impossible mind!). Essentially your original surface now has the appearance of no longer being flat but is curved and twisted in all sorts of weird and wonderful ways, one section near the top even has both opposing edges showing depth at the same point. Just coding the UI capture what distortions you wanted you be a bit of a challenge.

It's an optical illusion. All he did was rotate a few copies of the image xD

EDIT: suggestion. if you could implement some sort of "vanishing point" function, this would definitely beat trail by a long shot :) I'm no coder, though, so I wouldn't know how difficult that would be, but from a non-coder viewpoint, here's how I would do it: do exactly what it does now with the angle changer and all, but add the ability to choose how much smaller each repeat is compared to the previous.

Edited by pdnnoob

Share this post


Link to post
Share on other sites

Well I did say it was easier said than done!

However what I had imagined is far less involved than what you are considering. I would assume the simplest scenario, of flat text on a transparent layer (if the user wants to rotate it first that can already be done using 'rotate/zoom'). Personally I would only consider single point perspective (I've given up on 6 point fish-eye grid plugin, because it was turning my brain inside out!).

Then it would be a matter of finding the first non transparent pixel in a line towards the vanishing point and copying those values back along the line toward (or away from) the VP in a ratio to the distance from the VP. A different angle for nearly every pixel, and averaging values for all but those in a perpendicular or horizontal line. - something very similar to the 'zoom blur' plugin but with a greater range for the VP position. Hard enough to explain let alone make it work!

I wouldn't worry about retaining the original shape either, just recommend that people use a copied layer and move the first layer up later.

Anyway they're only my ideas, and I'm not sure if that this method is even feasible. I am far more of a beginner than you, judging by your code!

Personally I've not had much success with VS express and plugins, but I think others on here have and there might be a c# template somewhere on the developers central thread.

No worries over time - It is extremely generous of you to make and share plugins in the first place;)

Share this post


Link to post
Share on other sites

No - you are right. I think you would need 3 vanishing points to work out the length of 'smear'. As the attached picture shows, without a VP on the left or a nadir, all the angles look very wrong.

exp2.jpg

Edited by Red ochre

Share this post


Link to post
Share on other sites

No - you are right. I think you would need 3 vanishing points to work out the length of 'smear'. As the attached picture shows, without a VP on the left or a nadir, all the angles look very wrong.

With the method I mentioned, two of the vanishing points will be handled quite well by the rotate/zoom you did on the object beforehand. The last one is where the plugin comes in if you see what I mean... PDN already has all you need to get those first 2 vanishing points fixed up nicely. All you have to do is figure out how to get the third to match up using an imaginary plugin we don't yet have.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this