Rotating an image about a point other than the center

Rotating an image about a point other than the center

Postby onpon4 » Fri Oct 18, 2013 4:09 pm

Let me start by introducing my project. I have been developing a universal game engine, kind of similar to what IDEs like Game Maker, Game Editor, and Construct use, called the "SGE", which stands for "Stellar Game Engine" and is pronounced like "sage". More information can be found at the SGE's web page.[1]

I noticed recently that I never made the rotation mechanism used in the SGE rotate images about what is called the origin: the point where an image is positioned from, so I decided to implement this recently. Unfortunately, the way I have done it seems to be completely wrong. Without the new code I added (albeit with some fixes to the rotation, which was slightly off previously), images rotate perfectly about the center. With the new code which is intended to make the image rotate around the origin, however, the rotation is completely different, and what exactly happens is hard to describe.

To see the problem, checkout or download the Git repository from one of the Git repos[2][3] (they are currently the same; the GitHub repo will be dropped at some point in the future) and run examples/rotation.py. You will need Python 2 and Pygame 1.9 or later. You will see a few needles rotating; the point they should be rotating around is the head (the fatter parts of the needles), but instead they rotate in a completely different way which I don't really understand.

I have found that the problem is somewhere between lines 1651 and 1697 of sge-pygame/StellarClass.py. However, I ran through a test case using the method on paper with origin_x=18, origin_y=4, rotation=112, image_width=32, image_height=32 and got a result that seems right: an x offset of about -6.5 and a y offset of about 51.6, resulting in the origin being at about (11.5, 55.6). (In fact, I did find some problems with some calculations, but I fixed those, and the rotation is still wrong.)

Here is the code where the problem lies (a little bit more than the problem lines; the problem lines start at "if rotation % 360"):

Code: Select all
def _get_rotation_offset(origin_x, origin_y, rotation, image_width,
                         image_height, image_width_normal,
                         image_height_normal):
    # Return what to offset an origin when the object is rotated as a
    # two-part tuple: (x_offset, y_offset)
    x_offset = 0
    y_offset = 0

    if rotation % 180:
        # Adjust offset for the borders getting bigger.
        x_offset += (image_width - image_width_normal) / 2
        y_offset += (image_height - image_height_normal) / 2

    if rotation % 360:
        # Rotate about the origin
        center_x = image_width / 2
        center_y = image_height / 2
        x = origin_x - image_width
        # We have to make y negative to work with the unit circle.
        y = -(origin_y - image_height)

        if x or y:
            if not x:
                rot = math.radians(90 if y > 0 else 270)
                h = abs(y)
            elif not y:
                rot = math.radians(0 if x > 0 else 180)
                h = abs(x)
            else:
                rot = abs(math.atan(y / x))
                h = abs(y / math.sin(rot))

                # Find quadrant
                if y > 0:
                    if x > 0:
                        # Quadrant I; nothing to do
                        pass
                    else:
                        # Quadrant II
                        rot = math.pi - rot
                else:
                    if x < 0:
                        # Quadrant III
                        rot += math.pi
                    else:
                        # Quadrant IV
                        rot = (2 * math.pi) - rot

            rot += math.radians(rotation)
            rot %= 2 * math.pi
            new_x = h * math.cos(rot)
            new_y = h * math.sin(rot)
            new_origin_x = new_x + image_width
            # Now that we're done with the unit circle,
            # we need to change back to the SGE's
            # version of y.
            new_origin_y = -new_y + image_height

            x_offset += new_origin_x - origin_x
            y_offset += new_origin_y - origin_y

    return (x_offset, y_offset)


I use an "offset" method: x_offset and y_offset indicate how to offset the origin to get it in the right place.

Can anyone see a problem with the method I'm using that I've missed?

EDIT: I added some green circles to the test program; they show where the origin of each needle is (where the heads should reside).

[1] http://stellarengine.nongnu.org
[2] https://savannah.nongnu.org/git/?group=stellarengine
[3] https://github.com/coppolaemilio/stellargameengine
onpon4
 
Posts: 4
Joined: Fri Oct 18, 2013 3:44 pm

Re: Rotating an image about a point other than the center

Postby Mekire » Sun Oct 20, 2013 6:43 pm

This is unrefined, but maybe it will get you headed in the right direction.

https://github.com/Mekire/pygame-rotation

Just grab the repo and run rotate.py as main. Just shows Lena rotated about two different origin points.

-Mek
User avatar
Mekire
 
Posts: 986
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: Rotating an image about a point other than the center

Postby onpon4 » Sun Oct 20, 2013 10:29 pm

I'm going to be completely honest: that example itself gave me a headache. But anyway, I've got rotation about the origin now thanks to it. This is what I ended up with:

Code: Select all
def _get_rotation_offset(origin_x, origin_y, rotation, image_width,
                         image_height, image_width_normal,
                         image_height_normal):
    # Return what to offset an origin when the object is rotated as a
    # two-part tuple: (x_offset, y_offset)
    x_offset = 0
    y_offset = 0

    if rotation % 180:
        # Adjust offset for the borders getting bigger.
        x_offset += (image_width - image_width_normal) / 2
        y_offset += (image_height - image_height_normal) / 2

    if rotation % 360:
        # Rotate about the origin
        center_x = image_width_normal / 2
        center_y = image_height_normal / 2
        xorig = origin_x - center_x
        yorig = origin_y - center_y
        start_angle = math.atan2(-yorig, xorig)
        new_angle = start_angle + math.radians(rotation)
        radius = math.hypot(xorig, yorig)
        new_center_x = origin_x + radius * math.cos(new_angle)
        new_center_y = origin_y - radius * math.sin(new_angle)
        x_offset += new_center_x + center_x
        y_offset += new_center_y + center_y

    return (x_offset, y_offset)


The part that gave me the biggest headache was "x_mag" and "y_mag", because I couldn't for the life of me figure out what "mag" stood for and couldn't figure out what they meant until I considered the usage of math.atan2 to find the "start angle"; of course, it's the origin relative to the center instead of the top-left.

Thanks for the help, Mekire. :)
onpon4
 
Posts: 4
Joined: Fri Oct 18, 2013 3:44 pm

Re: Rotating an image about a point other than the center

Postby Mekire » Sun Oct 20, 2013 10:56 pm

In order to save processing you shouldn't recalculate the radius and start angle every frame (unless you need to). This is actually why I moved them out into a callable class. Also, not sure if you have it planned or not but I recommend you build rotation caching into your engine. Rotation can be an incredibly expensive process when done on mass.

-Mek
User avatar
Mekire
 
Posts: 986
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: Rotating an image about a point other than the center

Postby onpon4 » Mon Oct 21, 2013 1:27 am

Rotation caching is already in there (every transformation ever made to each image, and that includes rotation and scaling, is cached). Radius has to be recalculated each time unless it's cached; the position of a sprite's origin can change at any time. If the performance hit is significant, I guess I can cache radius and angle values for each origin position. Are square root and arc tangent calculations really that expensive?
onpon4
 
Posts: 4
Joined: Fri Oct 18, 2013 3:44 pm

Re: Rotating an image about a point other than the center

Postby Mekire » Mon Oct 21, 2013 7:38 am

Neither the radius nor the start angle need to change actually unless you are dynamically changing the radius. The location of the origin may change, but that doesn't effect the radius or the start angle. And no, the big cost is definitely the rotation/scaling itself; but if you can avoid recalling trig functions and powers without loss it is still worth it in my opinion. That kind of stuff can still add up.

Anyway, few minor edits to my version, including making one of the images move with the arrow keys. This demonstrates how even with a moving origin the radius and start angle are not recalced.

-Mek
User avatar
Mekire
 
Posts: 986
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: Rotating an image about a point other than the center

Postby metulburr » Mon Oct 21, 2013 12:04 pm

@onpon4
Sorry to hijack your thread

@mekire
might i suggest keep going with the pygame tutorials snippets on your github? It is quite a good pygame refresher or general tutorial, and you know how hard those are to find. I enjoy also skimming them too when i have been away from pygame/python awhile.
New Users, Read This
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
steam
User avatar
metulburr
 
Posts: 1387
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: Rotating an image about a point other than the center

Postby onpon4 » Mon Oct 21, 2013 2:45 pm

Mekire wrote:Neither the radius nor the start angle need to change actually unless you are dynamically changing the radius. The location of the origin may change, but that doesn't effect the radius or the start angle.


How can that be the case? What you called "x_mag" and "y_mag", and what I renamed to "xorig" and "yorig", is used to calculate both of these, and these two variables are calculated based on the location of the origin and the size of the image (where the center is, specifically). In short, the start angle is the angle on the unit circle if you assume that the center is the origin, and the radius is the distance of that point from (0, 0). So if the origin is at (0, 0) with an image that's 32x32, you'll have a start angle of 125 degrees and a radius of sqrt(512), but if the origin is at (18, 16) with that same image, the start angle will be 0 degrees and the radius will be 2.

Even if I misunderstood something, though, the radius can dynamically change. Images can be scaled and resized at any time.

Mekire wrote:And no, the big cost is definitely the rotation/scaling itself; but if you can avoid recalling trig functions and powers without loss it is still worth it in my opinion. That kind of stuff can still add up.


I'd have to highly disagree with that. It's not Pythonic to worry about performance hits if they're not a real problem.

I did some quick tests, and I found that these operations do take about 3 times longer than a few "normal" math operations, but I don't think this is inefficient enough to justify making a big deal about it; on my laptop, at 10 million repetitions, it took 6 seconds to do both a math.hypot call and a math.atan2 call as opposed to 2 seconds for one each of addition, subtraction, multiplication, and division. I took a look at checking a dictionary (the way I would cache results), too, and that took about 1 second. If I'm doing so many repetitions that a little basic arithmetic is taking up two seconds of time in a single frame, I clearly need to reduce the number of repetitions, so I don't really see any sense in caching these results in an attempt to make the function itself more efficient.

Anyway, thanks again for the help. :)
onpon4
 
Posts: 4
Joined: Fri Oct 18, 2013 3:44 pm

Re: Rotating an image about a point other than the center

Postby Mekire » Mon Oct 21, 2013 10:47 pm

Yes, resizing would require recalcing, especially if it actually changed the aspect ratio of the sprite; but this is another matter. My point is it doesn't need to be recalculated 60 times a frame (or whatever your target frame rate). I do agree that caching them would be going a bit far though. As for unpythonic; yes mindlessly focusing on tiny optimizations can be unpythonic, but that logic is not justification for writing inefficient algorithms (Edit: Harsher sounding than intended. I don't mean that yours is inefficient; just that simplicity in general shouldn't be used as an argument against efficiency). If you have numerous bodies on screen all making 60 calls to those functions a second though, it can easily cause frame loss. If you only have one or two, yeah it shouldn't matter much. I actually originally wrote mine as a single function that took all these arguments too until I realized that they didn't need to change.

I also wasn't sure if you were just talking about moving the origin point around but keeping the rotation axis the same (as in my example), or if you are trying to allow the user to change the sprites rotation axis at any time. The latter will get a little tricky and certainly require recalculation.

Anyway, good luck to you. Either way sounds like you have essentially solved your problem.
-Mek

Edit:
Updated my implementation. Change axis of rotation with left mouse click. Increase/decrease/stop rotation with 0/-/+ keys. Linear translation with arrow keys.
https://github.com/Mekire/pygame-rotation
User avatar
Mekire
 
Posts: 986
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan


Return to Game Development

Who is online

Users browsing this forum: Baldyr and 0 guests