Subpixel positioning by blending two sprites together (crossfading sprites)

Started by Snarky, Fri 30/09/2011 17:31:51

Previous topic - Next topic

Snarky

I'm wondering whether there's a way in AGS to blend two sprites together, so that for each pixel, the RGB and the alpha values of the result are the (weighted) average of the two input sprites.

To give an example, say I have two frames of an animation, each a sprite with alpha-antialiased edges. I'd essentially like to crossfade from one frame to the other. So the solid areas where they overlap remain solid, blending to the average of their colors, areas where one is transparent and the other solid become half-transparent, and so on. (You do not get this effect by drawing both sprites on top of each other at 50% opacity, because the output will only be 75% opaque in the areas where both the original were solid.)

I see two possible approaches: one is to grab the background (DynamicSprite.CreateFromBackground/DrawingSurface/ScreenShot), draw each sprite on a copy of it, and crossfade those two sprites. For this solution, if it's even possible, I'm worried about not interfering with other sprites, handling z-order etc. Also performance.

The other is to use a GUI and the AdditiveOpacity drawing option. If I can draw two sprites with a certain transparency level on top of each other, I think the additive opacity hack in AGS (instead of the proper way of stacking transparent layers, which would be to multiply the transparencies together) will give me the result I want. But AFAIK there's no way to use a GUI as an object sprite, respecting walkbehinds and all that, or even faking that effect, is there?

Before I go down either of these paths, does anyone know whether it's possible, and what might work best?

Calin Leafshade


Snarky

#2
Well, I went ahead and tried the first approach, and what do you know! It works!

... Except for one thing: I can't make it run properly on animated backgrounds. The problem seems to be that the frame changes after I do the rawdrawing, so it ends up being drawn to the wrong frame and doesn't show up until the next time that frame comes around. (This causes the animation to constantly jump back a few frames.) Is there a way around that?

Anyway, the idea I had was for a way to do very smooth subpixel movements, for example for parallax effects. The code does this by drawing the sprite twice. So, for example if you want to draw a sprite at X = 120.65, it draws one copy at X=120 and another copy at X=121, and blends the two copies together linearly, by drawing the first copy with 65% transparency on top of the other. In order for this to work correctly, each sprite has to flattened onto a copy of the background first, as explained in the first post.

(Actually, the code here has been designed to work with sprites that already have a manually created half-pixel-displaced version, as used in the parallax module, so in this example it would blend one copy at X=120.5 and one copy at X=121 at a 70/30 ratio. The basic case is simpler, and is handled by a commented-out section of code.)

How does it look? Well, see this simple demo animation, with two clouds using smooth subpixel movement, and one moving pixel-by-pixel:

[imgzoom]http://i.imgur.com/K9GoW.gif[/imgzoom]
(AGS demo)

I can't be bothered to make it a module right now, but if you're interested, here's the source code:

Code: ags

DrawingSurface* background;

// Draw a sprite at subpixel X-positions
void DrawSpriteSubX(int spriteslot,  int displacedspriteslot,  float x,  int y,  bool clear)
{
  // Backup and restore the plain background
  // By setting clear to false for subsequent sprites, you can draw multiple sprites on top of each other
  if(clear)
  {
    if(background == null)
    {
      DrawingSurface* screen = Room.GetDrawingSurfaceForBackground();
      background = screen.CreateCopy();
      screen.Release();
    }
    else
    {
      DrawingSurface* screen = Room.GetDrawingSurfaceForBackground();
      screen.DrawSurface(background);
      screen.Release();
    }
  }
  
  // Initialize drawing area dimensions
  int int_x = FloatToInt(x, eRoundDown);
  
  int width = Game.SpriteWidth[spriteslot]+1;
  int height = Game.SpriteHeight[spriteslot];
  
  // If we're trying to draw outside of the screen, skip
  if(int_x >= System.ViewportWidth || y >= System.ViewportHeight || int_x + width < 0 || y + height < 0)
    return;
  
  // Crop the drawing area to the screen size
  int offset_x = 0;
  int offset_y = 0;

  if(int_x<0)
  {
    offset_x = int_x;
    int_x = 0;
  }
  
  if(y<0)
  {
    offset_y = y;
    y = y;
  }
  
  if(int_x+width > System.ViewportWidth)
    width = System.ViewportWidth - int_x;
  if(y+height > System.ViewportHeight)
    height = System.ViewportHeight - y;

  // Get background, cropped to drawing area
  DynamicSprite* bg = DynamicSprite.CreateFromBackground(GetBackgroundFrame(), int_x, y, width, height);
  
  // Make two copies to draw the sprites on
  DrawingSurface* ds1 = bg.GetDrawingSurface();
  DrawingSurface* ds2 = ds1.CreateCopy();
  
  float frac_x = x - IntToFloat(int_x+offset_x);
  
  // Draw the sprites on each copy, then draw the displaced one on top with transparency
  /*
  ds1.DrawImage(offset_x, offset_y, spriteslot);
  ds2.DrawImage(offset_x+1, offset_y, displacedspriteslot);
  int trans = 100 - FloatToInt(frac_x * 100.0, eRoundNearest);
  if(trans<100)
    ds1.DrawSurface(ds2, trans);
  */
  
  ds2.DrawImage(offset_x, offset_y, displacedspriteslot);
  
  
  if(frac_x < 0.5)
  {
    ds1.DrawImage(offset_x, offset_y, spriteslot);
    int trans = 100 - FloatToInt(frac_x * 200.0, eRoundNearest);
    DebugLabel.Text = String.Format("X: %.2f[%d.0: %02d%%[%d.5: %02d%%", x, int_x, trans, int_x, 100-trans);
    if(trans < 100)
      ds1.DrawSurface(ds2, trans);
  }
  else
  {
    ds1.DrawImage(offset_x + 1, offset_y, spriteslot);
    int trans =  FloatToInt((frac_x - 0.5) * 200.0, eRoundNearest);
    DebugLabel.Text = String.Format("X: %.2f[%d.5: %02d%%[%d.0: %02d%%", x, int_x, 100-trans, int_x+1, trans);
    if(trans<100)
      ds1.DrawSurface(ds2,trans);
  }
  
  
  ds1.Release();
  ds2.Release();
  
  // Draw the merged sprite onto the background
  DrawingSurface* ds = Room.GetDrawingSurfaceForBackground();
  ds.DrawImage(int_x, y, bg.Graphic);
  bg.Delete();
  ds.Release();
}


And here's how I call it:
Code: ags

float cloudpos1 = 200.0;
float speed1 = -0.06;
float cloudpos2 = 290.0;
float speed2 = -0.15;

function room_RepExec()
{
  cloudpos1 += speed1;
  if(FloatToInt(cloudpos1) + Game.SpriteWidth[1] < - 10)  // The clouds wrap around
    cloudpos1 = 330.0;
  
  cloudpos2 += speed2;
  if(FloatToInt(cloudpos2) + Game.SpriteWidth[1] < - 10)  // The clouds wrap around
    cloudpos2 = 330.0;

  DrawSpriteSubX(1, 2, cloudpos1, 78);
  DrawSpriteSubX(3, 4, cloudpos2, 20,  false);  // clear is false so this cloud is drawn on top of the other one
}

Calin Leafshade

That is very cool.

Very clever way of hacking sub pixel movement

ThreeOhFour

Totally agree! Definitely something to keep in mind, I've tried faking subpixel animation before with only marginal success.

Wyz

Wow, this is a really neat trick. Thanks for finding out! :D
Life is like an adventure without the pixel hunts.

Snarky

Thanks guys. I was psyched that it actually worked. It's the first time I've worked with RawDrawing and dynamic sprites, and I didn't know what they were and were not capable of.

Because of how it's implemented, there are a few limitations:


  • It doesn't work on animated backgrounds (at least not yet).
  • It doesn't respect walkbehinds. In the demo the house, and also the trees on the left, are therefore objects.
  • It can't be in front of any "real" object or character (or overlay or GUI).
  • The above two points together can lead to strange effects if it overlaps with an object that is partly behind a walkbehind. You could get around the first one by drawing the dynamic sprite to an object graphic instead of to the background, but then you couldn't have multiple subpixel-positioned sprites on top of each other, and you might accidentally draw the background on top of any objects behind it in the z-order.
  • And although the movement is smooth, at low resolutions it creates a slight "shimmering" effect that some might not like.

While this code only does X-axis subpixels, it should be easy to extend to two dimensions. Then you have to draw each sprite four times and do three merges, though.

It seems to work better if you use two source sprites that are scaled down from double the resolution. Using only the basic sprite, or generating/faking a 0.5 pixel displaced version of it manually (by scaling it up, moving it one pixel and scaling back down) looks a bit more blurry, I think.

SMF spam blocked by CleanTalk