pybox2d / pybox2d

2D Game Physics for Python
Other
474 stars 93 forks source link

vertex locations change when setting vertices for polygon #109

Open Chrispresso opened 4 years ago

Chrispresso commented 4 years ago

Let's say you want to create a simple triangle with vertices at [(0, 0), (1, 0), (0, 1)]. According to the documentation you need to define the vertices in a CCW manner, which I have done starting at (0, 0).

In [57]: triangle = b2PolygonShape(vertices=[(0,0), (1,0), (0,1)])

In [58]: triangle
Out[58]: b2PolygonShape(vertices: [(1.0, 0.0), (0.0, 1.0), (0.0, 0.0)])

So why would the vertices change when I set them?

Another example:

In [62]: verts = [(0, 0), (1, 0), (1, -1), (0, -1)]

In [63]: square = b2PolygonShape(vertices=verts)

In [64]: square
Out[64]: b2PolygonShape(vertices: [(1.0, -1.0), (1.0, 0.0), (0.0, 0.0), (0.0, -1.0)])

The reason this is a problem for me: let's say we have that square and we want to rotate it 10 degrees based off the origin (0, 0).

In [70]: import math
In [71]: def deg2rad(degree):
    ...:     return degree * math.pi / 180.
    ...:
In [74]: def rotate_poly(coords, center, angle):
    ...:     rads = deg2rad(angle)
    ...:     new_coords = []
    ...:     for coord in coords:
    ...:         new_coord = b2Vec2()
    ...:         new_coord.x = math.cos(rads)*(coord.x - center.x) - math.sin(rads)*(coord.y - center.y) + center.x
    ...:         new_coord.y = math.sin(rads)*(coord.x - center.x) + math.cos(rads)*(coord.y - center.y) + center.y
    ...:         new_coords.append(new_coord)
    ...:
    ...:     return new_coords
    ...:
In [81]: verts = [b2Vec2(coord[0], coord[1]) for coord in verts]

In [82]: verts
Out[82]: [b2Vec2(0,0), b2Vec2(1,0), b2Vec2(1,-1), b2Vec2(0,-1)]

Now see what happens when I rotate verts:

In [83]: rotate_poly(verts, b2Vec2(0,0), 10)
Out[83]:
[b2Vec2(0,0),
 b2Vec2(0.984808,0.173648),
 b2Vec2(1.15846,-0.81116),
 b2Vec2(0.173648,-0.984808)]

Notice how my origin (0, 0) remains in the same location

Now see what happens when I rotate square by 10:

In [85]: square = b2PolygonShape(vertices=rotate_poly(verts, b2Vec2(0, 0), 10))

In [86]: square
Out[86]: b2PolygonShape(vertices: [(1.1584559679031372, -0.8111595511436462), (0.9848077297210693, 0.1736481785774231), (0.0, 0.0), (0.1736481785774231, -0.9848077297210693)])

Notice how my origin (0, 0) is now at index 2 in the vertices list.

Now see what happens when I rotate square by -10:

In [87]: square = b2PolygonShape(vertices=rotate_poly(verts, b2Vec2(0, 0), -10))

In [88]: square
Out[88]: b2PolygonShape(vertices: [(0.9848077297210693, -0.1736481785774231), (0.0, 0.0), (-0.1736481785774231, -0.9848077297210693), (0.8111595511436462, -1.1584559679031372)])

Notice how my origin (0, 0) is now at index 1.

There does not seem to be a reason for this. I would expect the order I add my vertices in to be the order they remain in. This becomes problematic when I'm trying to grab my origin point because I have to search for it in the indices list and because of this I believe this is a bug (correct me if I'm wrong).

In [99]: Box2D.__version__
Out[99]: '2.3.2'
Chrispresso commented 4 years ago

It seems like looking at the unit tests, copying and pasting the code for the most part causes it to raise the exception:

In [191]: def dotest(world, v):
     ...:         body = world.CreateDynamicBody(position=(0,0),
     ...:                         shapes=b2PolygonShape(vertices=v) )
     ...:         for v1, v2 in zip(v, body.fixtures[0].shape.vertices):
     ...:             if v1 != v2:
     ...:                 raise Exception('Vertices before and after creation unequal. Before and after zipped=%s'
     ...:                         % list(zip(v, body.fixtures[0].shape.vertices)))
     ...:
     ...:
     ...:

In [192]: dotest(world, verts)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-192-5b3ffb9083aa> in <module>
----> 1 dotest(world, verts)

<ipython-input-191-14b01600984f> in dotest(world, v)
      5             if v1 != v2:
      6                 raise Exception('Vertices before and after creation unequal. Before and after zipped=%s'
----> 7                         % list(zip(v, body.fixtures[0].shape.vertices)))
      8
      9

Exception: Vertices before and after creation unequal. Before and after zipped=[(b2Vec2(0,0), (1.0, -1.0)), (b2Vec2(1,0), (1.0, 0.0)), (b2Vec2(1,-1), (0.0, 0.0)), (b2Vec2(0,-1), (0.0, -1.0))]

I'm able to verify that if I put the vertices in the order that they are rearranged to, the exception does not get raised:

In [198]: dotest(world, [b2Vec2(1, 0), b2Vec2(0, 1), b2Vec2(0, 0)])

In [199]:
kne commented 4 years ago

It appears that the vertices are modified by the gift wrapping algorithm in Box2D itself. pybox2d, as best I can tell, is dutifully passing along the vertices as you have specified.

https://github.com/pybox2d/pybox2d/blob/070d908c49032c5c30f31eac543c6f6691799afc/Box2D/Collision/Shapes/b2PolygonShape.cpp#L138-L139

http://en.wikipedia.org/wiki/Gift_wrapping_algorithm

Not sure the best course of action for your specific case, though. Given that your issue is pretty old, chances are you don't much care about this anymore :)

Chrispresso commented 4 years ago

I still (kinda) care. The way I got around it was by accepting I couldn't change it and tracking a unique point. Then I knew where my rotation point of the array was. It's not a big deal that I need to track it, but it's a bit of a pain to debug when you expect points in a certain order.

kne commented 4 years ago

I can understand that.

I'd rather not change the behavior of Box2D itself (that is, as in the algorithm I referenced above), but would not be against adding a utility function or two to help in this scenario.