Rotating an object around a pivot point.

Started by Scavenger, Sat 17/06/2017 07:36:50

Previous topic - Next topic

Scavenger

I'm trying to create a bell in-engine, which swings back and forth, but I'm having a little bit of trouble - I can rotate the sprite around its central point, and I can move the object in a circle, but I can't sync them up. I don't think I'm good enough at math to work it out.

The sprite is here:

It should rotate around the middle of the hole at the top, so I can get it to swing. My current code is:

Code: AGS
DynamicSprite *bellsprite;
int angle = 1;
function room_Load()
{
bellsprite = DynamicSprite.CreateFromExistingSprite (520);
}

function room_RepExec()
{
  angle++;
  if (angle > 359) angle = 1;
bellsprite = DynamicSprite.CreateFromExistingSprite (520);
bellsprite.Rotate (angle, 128, 128);
oBell.Graphic = bellsprite.Graphic;
oBell.X = FloatToInt(((100.0-64.0) * Maths.Cos (Maths.DegreesToRadians (IntToFloat (angle)))) - ((113.0-32.0) *  Maths.Sin (Maths.DegreesToRadians (IntToFloat (angle)))))+64;
oBell.Y = FloatToInt(((100.0-64.0) * Maths.Sin (Maths.DegreesToRadians (IntToFloat (angle)))) + ((113.0-32.0) *  Maths.Cos (Maths.DegreesToRadians (IntToFloat (angle)))))+32;
}


But this is incredibly wrong, I know it. I just don't know how to make it go. Can anyone give me some advice?

Snarky

#1
A rotation around an arbitrary point is usually calculated as:

-Translate the coordinate system so the pivot point is at 0,0
-Rotate
-Translate back

What you need to do is to work out the net effect of the two translations.

If the coordinates of your pivot are (pivotX,pivotY), the first translation is (-pivotX,-pivotY) and the second is (pivotX,pivotY). So we need to calculate:

Code: ags
// translate pivot (pivotX,pivotY) to origin, rotate, and return
{
  float pivotTranslation[] = new float[2];
  pivotTranslation[0] = -pivotX;
  pivotTranslation[1] = -pivotY;

  pivotTranslation = Rotate2D(pivotTranslation, angle);
  pivotTranslation[0] = pivotTranslation[0] + pivotX;
  pivotTranslation[1] = pivotTranslation[1] + pivotY;
}


The formula for calculating the x and y coordinates of a point after a rotation by a certain angle is (according to the first result that came up when I googled it):

Code: ags
float[] Rotate2D(float[] coords, float angle)
{
  float newcoords[] = new float[2];
  newcoords[0] = coords[1]*Maths.Sin(angle) + coords[1]*Maths.Cos(angle);  
  newcoords[1] = coords[1]*Maths.Cos(angle) - coords[0]*Maths.Sin(angle);
  return newcoords;
}


There's a further complication in that rotating the sprite changes its dimensions, so to keep the center centered you have to adjust the position. Depending on how you're doing that currently it might look a little different.

Note that because the pivot may not lie on an integer coordinate after rotation, you're probably going to get a bit of "wobbling" around it when you eventually convert the floats back to ints.

Mandle

Couldn't you just extend the top of the sprite as a purple transparent area so that the hole in the ring becomes the center of the sprite?

Khris

Here's working code:
Code: ags
DynamicSprite *bellsprite;
int angle = 0;
int bellSlot = 520;

function room_Load()
{
  bellsprite = DynamicSprite.CreateFromExistingSprite(bellSlot);
}

int bellX = 100, bellY = 113; // lower left corner coords (angle 0)
int px = 63, py = 23; // sprite's pivot cords

function room_RepExec()
{
  angle = (angle + 1) % 360;
  
  // set sprite and store dimensions
  bellsprite = DynamicSprite.CreateFromExistingSprite(bellSlot);
  int w = bellsprite.Width;
  int h = bellsprite.Height;
  
  // rotate if necessary
  if (angle > 0) bellsprite.Rotate (angle);
  oBell.Graphic = bellsprite.Graphic;
  
  // calculate sprite dimension change offset
  float xOff = IntToFloat(w - bellsprite.Width) / 2.0;
  float yOff = IntToFloat(bellsprite.Height - h) / 2.0;
  
  // calculate vector from pivot to center
  float vx = IntToFloat(w) / 2.0 - IntToFloat(px);
  float vy = IntToFloat(h) / 2.0 - IntToFloat(py);
  // rotate vector
  float rad = Maths.DegreesToRadians (IntToFloat (angle));
  float cos = Maths.Cos(rad);
  float sin = Maths.Sin(rad);
  float vrx = vx * cos - vy * sin;
  float vry = vx * sin + vy * cos;
   
  oBell.X = bellX + FloatToInt(xOff - vx + vrx, eRoundNearest);
  oBell.Y = bellY + FloatToInt(yOff - vy + vry, eRoundNearest);
}


There are two offsets:
1) the rotation increases the sprite, so that has to be corrected
2) moving the pivot away from the center, like Snarky explained

Mandle

Amazing! But couldn't you just extend the top of the sprite as a... (see above)

Or was I wrong about how that might work?

Snarky

Yeah, you could, but code gives you more flexibility (e.g. if you want to rotate around different pivots). Also, if the pivot is far from the center, the sprite will have to be a lot bigger, and this will probably slow it down further.

Mandle

Quote from: Snarky on Mon 19/06/2017 16:00:08
Yeah, you could, but code gives you more flexibility (e.g. if you want to rotate around different pivots). Also, if the pivot is far from the center, the sprite will have to be a lot bigger, and this will probably slow it down further.

I see... What I was really after was that feeling of finding the keys in the sun-visor while you guys were trying to  hotwire the car... (laugh):P

Snarky

Yeah, it's definitely a clever workaround. When you mentioned it I was annoyed that I overlooked such a simple alternative.

Mandle

Quote from: Snarky on Tue 20/06/2017 00:05:12
Yeah, it's definitely a clever workaround. When you mentioned it I was annoyed that I overlooked such a simple alternative.

I always follow the path of least work first if possible... :-D

Scavenger

I tried both methods, both Khris' code and expanding the sprite so that the pivot point was in the center. The latter method was a lot smoother, the code made the bell jitter around very unpleasantly, which I guess is the problem when you've got a lot of integer math going on.

Snarky

Well, Khris's code is all in floats, but the problem is in the rounding.

The issue is that when you rotate the sprite, the corners go outside the original region, and therefore the new sprite has to be bigger to fit it. Even if it's only a tiny fraction of a pixel outside your "canvas", you of course have to add a whole row or column of pixels. This can shift the whole sprite almost a whole pixel right or down, leading to jittering.

When your pivot is at the center of the sprite, symmetry ensures that you add equally many pixels on each side in the rotated sprite, so by re-centering it the pivot stays in the same position, and you avoid any jittering. But when your pivot is not at the center this does not hold, so the sprite will jump around by about a pixel.

Given that AGS doesn't allow sub-pixel positioning of sprites, I really can't think of any way around it.

Khris

The only way to create smooth and custom rotations is to calculate the rotated sprite yourself. That's actually what I did for my MarioKart demo, since the built in Rotate function causes jittering for large sprites due to rounding errors.
Unfortunately, doing it in real-time is pretty slow.

Mandle

Quote from: Scavenger on Tue 20/06/2017 20:01:10
I tried both methods, both Khris' code and expanding the sprite so that the pivot point was in the center. The latter method was a lot smoother, the code made the bell jitter around very unpleasantly, which I guess is the problem when you've got a lot of integer math going on.

Hehe, there's those keys! :P 8-)

Glad to hear my stop-gap solution helped out! A very nice feeling indeed!

Can't wait to play the game now even if just to see the bell in action... (laugh)

SMF spam blocked by CleanTalk