Search code examples
vectorxnarotationsprite

XNA - Bounding Box Rotation Nightmare


I'm currently going throught kind of a nightmare right now by trying to find the right formula to obtain a bounding box that correspond to my sprite orientation.

I KNOW ! There is a bunch of examples, solution, explanations on the internet, including here, on this site. But trust me, I've tried them all. I tried to just apply solutions, I tried to understand explanations, but every post gives a different solution and none of them work.

I'm obviously missing something important here...

So, basically, I have a sprite which texture is natively (20 width * 40 height) and located at (200,200) when starting the app. The sprite origin is a classic

_origin = new Vector2((float)_texture.Width / 2, (float)_texture.Height / 2);

So origin would return a (5.5;8) vector 2

By keyboard input, I can rotate this sprite. Default rotation is 0 or Key.Up. Then rotation 90 corresponds to Key.Right, 180 to Key.Down, and so on...

For the moment, there is no move involved, just rotation.

So here is my code to calculate the bounding rectangle:

public partial class Character : ICollide
{
    private const int InternalRunSpeedBonus = 80;
    private const int InternalSpeed = 80;
    private Vector2 _origin;
    private Texture2D _texture;
    private Texture2D _axisBase;
    private Texture2D _axisOrig;

    public Character()
    {
        MoveData = new MoveWrapper { Rotation = 0f, Position = new Vector2(200, 200), Speed = new Vector2(InternalSpeed) };
    }

    public MoveWrapper MoveData { get; set; }

    #region ICollide Members

    public Rectangle Bounds
    {
        get { return MoveData.Bounds; }
    }

    public Texture2D Texture
    {
        get { return _texture; }
    }
    #endregion ICollide Members

    public void Draw(SpriteBatch theSpriteBatch)
    {
        theSpriteBatch.Draw(_texture, MoveData.Position, null, Color.White, MoveData.Rotation, _origin, 1f, SpriteEffects.None, 0);//main sprite
        theSpriteBatch.Draw(_axisOrig, MoveData.Position, null, Color.White, 0f, _origin, 1f, SpriteEffects.None, 0);//green
        theSpriteBatch.Draw(_axisBase, MoveData.Position, null, Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0);//red

    }

    public void Load(ContentManager theContentManager)
    {
        _texture = theContentManager.Load<Texture2D>("man");
        _axisBase = theContentManager.Load<Texture2D>("axis");
        _axisOrig = theContentManager.Load<Texture2D>("axisOrig");
        _origin = new Vector2((float)_texture.Width / 2, (float)_texture.Height / 2);

    }

    public void MoveForward(GameTime theGameTime, KeyboardState aCurrentKeyboardState)
    {
        InternalMove(theGameTime, aCurrentKeyboardState);
    }

    private void InternalMove(GameTime theGameTime, KeyboardState aCurrentKeyboardState, bool forward = true)
    {
        //stuff to get the move wrapper data valorized (new position, speed, rotation, etc.)
        MoveWrapper pm = MovementsHelper.Move(MoveData.Position, MoveData.Rotation, aCurrentKeyboardState, InternalSpeed,
                                              InternalRunSpeedBonus, theGameTime, forward);
        pm.Bounds = GetBounds(pm);
        MoveData = pm;
    }

    public void MoveBackward(GameTime theGameTime, KeyboardState aCurrentKeyboardState)
    {
        InternalMove(theGameTime, aCurrentKeyboardState, false);

    }

    private Rectangle GetBounds(MoveWrapper pm)
    {
        return GetBoundingBox(pm, _texture.Width, _texture.Height);
    }

    public  Rectangle GetBoundingBox(MoveWrapper w, int tWidth, int tHeight)
    {
        //1) get original bounding vectors

        //upper left => same as position
        Vector2 p1 = w.Position;

        //upper right x = x0+width, y = same as position
        Vector2 p2 = new Vector2(w.Position.X + tWidth, w.Position.Y);

        //lower right x = x0+width, y = y0+height
        Vector2 p3 = new Vector2(w.Position.X + tWidth, w.Position.Y + tHeight);

        //lower left x = same as position,y = y0+height
        Vector2 p4 = new Vector2(w.Position.X, w.Position.Y + tHeight);


        //2) rotate all points given rotation and origin
        Vector2 p1r = RotatePoint(p1, w);
        Vector2 p2r = RotatePoint(p2, w);
        Vector2 p3r = RotatePoint(p3, w);
        Vector2 p4r = RotatePoint(p4, w);

        //3) get vector2 bouding rectancle location
        var minX = Math.Min(p1r.X, Math.Min(p2r.X, Math.Min(p3r.X, p4r.X)));
        var maxX = Math.Max(p1r.X, Math.Max(p2r.X, Math.Max(p3r.X, p4r.X)));

        //4) get bounding rectangle width and height
        var minY = Math.Min(p1r.Y, Math.Min(p2r.Y, Math.Min(p3r.Y, p4r.Y)));
        var maxY = Math.Max(p1r.Y, Math.Max(p2r.Y, Math.Max(p3r.Y, p4r.Y)));

        var width = maxX - minX;
        var height = maxY - minY;

        // --> begin hack to get it work for 0,90,180,270 degrees
        var origMod = new Vector2((float)tWidth / 2, (float)tHeight / 2);

        var degree = (int)MathHelper.ToDegrees(w.Rotation);

        if (degree == 0)
        {
            minX -= origMod.X;
            minY -= origMod.Y;
        }
        else if (degree == 90)
        {
            minX += origMod.Y;
            minY -= origMod.X;
        }
        else if (degree == 180)
        {
            minX += origMod.X;
            minY += origMod.Y;
        }
        else if (degree == 270)
        {
            minX -= origMod.Y;
            minY += origMod.X;
        }
        // end hack <--

        return new Rectangle((int)minX, (int)minY, (int)width, (int)height);
    }

    public  Vector2 RotatePoint(Vector2 p, MoveWrapper a)
    {
        var m = Matrix.CreateRotationZ(a.Rotation);

        var refToWorldOrig = p - a.Position;
        Vector2 rotatedVector = Vector2.Transform(refToWorldOrig, m);
        var backToSpriteOrig = rotatedVector + a.Position;
        return backToSpriteOrig;

        //does not work
        //var Origin = new Vector3(_origin, 0);
        //var Position = new Vector3(p, 0);

        //var m = Matrix.CreateTranslation(-Origin)
        //        * Matrix.CreateRotationZ(a.Rotation)
        //        * Matrix.CreateTranslation(Position);

        //return Vector2.Transform(p, m);
    }
}

The rotation paramter is MathHelper degree to radians result.

I have a function to draw a rectangle corresponding to the bounding box and I expect that bounding box to overlap exactly with my sprite, at least for 0,90,180 and 270 degrees angle rotations.

Instead I have strange coordinates after rotation calculation: - when rotation to 90°, bounding box X is negative (so the box is not visible) - when rotation to 180°, bounding box X and Y are negative (so the box is not visible) - when rotation to 270°, bounding box Y is negative (so the box is not visible)

Can someone explain to me what I'm doing wrong, and do it like is was explaining to 3 year old child, because regarding Maths, this is what I am !!!

:)

EDIT : I have found a hack to make it work for 0, 90, 180, 270 degrees but now i'm stuck for intermediate positions (45,135,215, 325 degrees) which make me thinks that THERE MUST BE a way to compute all that stuff in one single formula that would work for any angle...

correct behavior for 90° rotation


Solution

  • Finally found the way to make it work without the hack !!!!!!!!!!!!!!!!

        public Vector2 RotatePoint(Vector2 p, MoveWrapper a)
        {
    
            var wm = Matrix.CreateTranslation(-a.Position.X - _origin.X, -a.Position.Y - _origin.Y, 0)//set the reference point to world reference taking origin into account
                     * Matrix.CreateRotationZ(a.Rotation) //rotate
                     * Matrix.CreateTranslation(a.Position.X, a.Position.Y, 0); //translate back
    
            var rp = Vector2.Transform(p, wm);
            return rp;
        }
    

    Bonus effect, this is even more precise (as my drawn guides seems to show) than my previous "hacky" method

    I juste realized that this is pretty close as what Blau proposed except that my first translation set the reference back to world 0,0,0 minus the sprite origin. I Guess id did not understand the hint at that time...