Inconsistent behavior with holes that share a point
GoogleCodeExporter opened this issue · 17 comments
Hi,
I am seeing inconsistent behavior with holes that share a vertex. Everything
here has been sanitized by clipper and guaranteed to have a 'strictly simple'
output.
Example #1. This triangulates fine:
Outline
10, 0
10, 10
0, 10
0,0
Hole 1
2, 1
3, 3
1, 2
Hole 2
3, 3
6, 4
4, 7
Example #2. This causes a segmentation fault.
Outline
374000, 96000
374000, 317000
576000, 317000
576000, 96000
Hole 1
449000, 291000
393000, 205000
419000, 177000
Hole 2
539000, 138000
419000, 177000
420000, 139000
In both examples, the holes share one point.
Why does one crash, but the other doesn't?
Original issue reported on code.google.com by buckyballreaction
on 20 Dec 2013 at 5:51
The thing is that two holes that share a point actually form a weak polygon. I
can understand the problem, clipper does make all polygons strictly simple but
when you add two of these as holes you are actually creating a weak polygon out
of two simple one's.
Anyways poly2tri should be able to support these cases if when you create the
points you make sure that shared points are one unique object.
Does hole 1 and hole 2 share the same point object for the shared point?
One dirty solution would be to shrink the holes a tiny amount that isn't
visible to the naked eye. A simple way would be to loop thru the vertexes of
each hole and move it them tiny bit:
For a 3 vertex hole the unrolled loop would look like:
p0 = p0 + 2e-12*normalize(p1 - p0);
p1 = p1 + 2e-12*normalize(p2 - p1);
p2 = p2 + 2e-12*normalize(p0 - p2);
By moving the p0 closer to p1 along the edge they form you do a form of shrink
that ensures that no new intersection between the holes will form when you
"wiggle" each hole vertex.
The factor has to be atleast twice the size of the epsilon used given that all
points is in the range [-1,1].
Original comment by thahlen@gmail.com
on 20 Dec 2013 at 11:14
The easiest way would be if you could make clipper return a list of all points
and the polygons would just be list of indexes to this list of points.
Original comment by thahlen@gmail.com
on 20 Dec 2013 at 5:46
Ah OK, that makes sense (I also read up a bit more on the sweep-line
algorithm). Although I'm still a little curious as to why both of my examples
above don't fail - could it be because of the resolution or accuracy of the
sweep-line?
Clipper does not use the same object instance for each Point, there is almost
no pointer usage, everything is on the stack.
Also, here is the documentation it gives on its output with respect to
weakly/strictly simple polygons:
http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes
/Clipper/Properties/StrictlySimple.htm
It looks like output polygons are guaranteed to be simple in some form, or
guaranteed to be strictly simple if you set the flag. No guarantee or option
of weakly-only, however. In my code we use the StrictlySimple(true) flag
because it reduced the crashing cases significantly; just the one in my opening
post remains so far.
Also, when I manually adjust the two holes, in the crashing case, into one hole
to form a weakly simple polygon by clipper's definition, it still crashes:
hole:
393000, 205000
419000, 177000
420000, 139000
539000, 138000
419000, 177000
449000, 291000
Original comment by buckyballreaction
on 20 Dec 2013 at 6:19
Hi,
I'm not sure I fully understand the problem... but you are saying that if I can
guarantee weakly-simple polygons (one unique object) for holes that share a
vertex, then poly2tri should handle them?
I'd like to avoid shrinking as it'll add more computation requirements to an
already expensive process (which is why we chose poly2tri to begin with - it is
fast!). Also, with our use case, there's no guarantee that shrinking won't
create the exact same situation since we have to feed very dirty data into
clipper.
I'll have to study clipper a bit more to see what it can do in this respect.
Original comment by buckyballreaction
on 20 Dec 2013 at 4:22
Check if the holes polygons created by clipper use same object instance for
shared points or if clipper creates a new point for every hole.
The problem if you have two different point instances with same coordinate is
that the graph of connected points poly2tri uses during triangulation will not
be correct and the triangulation can fail due to this.
Think of the triangulation as this.
You have a list of unique points to triangulate.
You can add some constraints that some triangle edges must exist between some
points.
This is all poly2tri cares about.
When you add holes what you actually do is add more points to the list of
unique points. Then enforce some edges between points in this list.
What makes poly2tri a polygon triangulator is the knowledge that we have a
closed loop of constraints forming an outer hull. With this knowledge we can
pick a triangle inside and loop thru all triangles and just save those inside
this hull.
Original comment by thahlen@gmail.com
on 20 Dec 2013 at 5:44
Perhaps I'm not thinking out-of-the-norm enough to understand (please forgive
me).
Are you saying that the hole that crashes above can still be triangulated if
fed properly into poly2tri? If so, what would the geometry look like? Can
just simple list of (in this case, 5) points suffice for input?
Original comment by buckyballreaction
on 20 Dec 2013 at 10:48
You would have to build you hole like this to support shared points
pseudo code:
list = { new Point( 393000, 205000 ),
new Point( 419000, 177000 ),
new Point( 420000, 139000 ),
new Point( 539000, 138000 ),
new Point( 449000, 291000 ) };
hole1 = { list[4], list[0], list[1] };
hole2 = { list[3], list[1], list[2] };
Most likely you will have to spend more time remapping points than it would
take to just shrink the holes like I wrote above. Which is a pretty straight
forward and fast fix. You could probably skip normalization of the vector if
you know your line segments isn't to short or long.
Since I really don't know what you are doing before and after triangulation I
can't give any better suggestions than this.
Original comment by thahlen@gmail.com
on 20 Dec 2013 at 11:47
Adding the same point twice as different instances will make poly2tri behave in
an undefined manner. Sometime it will work and sometimes not, it all depends on
how the two identical points end up in the advancing front and how the
constraints are directed. The connected graph will be flawed but under some
circumstances the triangulation will work anyways.
This is why poly2tri only supports simple polygons.
Given you can enforce that holes just share unique common points, poly2tri can
triangulate these polygons.
Original comment by thahlen@gmail.com
on 20 Dec 2013 at 6:45
Very interesting... now I understand what you meant by "shared points are one
unique object".
I will implement the shrinking and see how everything goes. I will also
continue the discussion about unique Point objects as an output of clipper.
Thanks again!
Original comment by buckyballreaction
on 21 Dec 2013 at 7:13
What you would have after the shrink operation is this overly exaggerated.
Original comment by thahlen@gmail.com
on 21 Dec 2013 at 12:15
Attachments:
Using the clipper OffsetPolygon class to shrink the polygons slightly added
about 50% processing time overhead, but did resolve the issues. This was a
little too much overhead for my tastes.
I did, however, adapt your 'wiggle-shrink' algorithm to our situation. Since
we upscale our points into clipper by 1000, all I needed to do was shrink the
output back along the line by 1. I can do it in-place with the clipper output
like so:
// Shrink large polygons by reducing each coordinate by 1 in the
// general direction of the last point as we wind around
static void edgeShrink(Path &path) {
unsigned int prev = path.size() - 1;
for(unsigned int i = 0; i < path.size(); i++) {
// Adjust coordinate by 1 depending on the direction
path[i].X - path[prev].X > 0 ? path[i].X-- : path[i].X++;
path[i].Y - path[prev].Y > 0 ? path[i].Y-- : path[i].Y++;
prev = i;
}
}
This simple method didn't add measurable difference to the computation time and
the crashing case is also solved.
Good idea!
Original comment by buckyballreaction
on 21 Dec 2013 at 10:42
Great! :)
Original comment by thahlen@gmail.com
on 21 Dec 2013 at 11:37
- Changed state: WontFix
I mean that the solution proposed by thahlen in comment #8 sounds not too
inefficient. I would build such a map:
std::map<std::pair<long,long>, Point*> unique_points;
and then loop through Clipper's output to populate it or reuse existing Point
pointers if the coordinate pair was already seen.
Since I would like to go this way, I was wondering why you thought that
manipulating coordinates to perform shrinking was more robust. I would be
afraid of the existence of another point at the new, shifted, coordinates...
From what I understood from thahlen's words, poly2tri is not afraid of polygons
or holes sharing a point, it's just afraid of duplicate allocated point
structures with same coordinates - which is very good news and a lot less
strict than I thought since an easy workaround can be applied!
Original comment by motond...@gmail.com
on 30 Apr 2014 at 6:34
buckyballreaction, why didn't you choose to remap points instead?
Using std::map and std::pair that should have been quite fast, and it looks
like it's more robust than messing with coordinates. What do you think?
Original comment by motond...@gmail.com
on 30 Apr 2014 at 4:02
I'm not sure I understand what you mean by 're-map' points.
The original issue was due to a conflict between having duplicate points in the
input but poly2tri requiring only one instance of each point.
My solution was a quick way to guarantee no duplicate points with the output
from Clipper.
Original comment by buckyballreaction
on 30 Apr 2014 at 4:28
Okay, thanks for feedback. I understand your performance needs. In my case I
prefer robustness over speed. Not sure I'll do comparisons, but will definitely
share my code if I'm going this way.
Original comment by motond...@gmail.com
on 30 Apr 2014 at 8:15
Ah OK. I understand now. That may definitely be a more exact solution.
In my case, however, speed is paramount. I'm doing large-scale triangulation
during the level loading for my game. Before my above code I had already
up-scaled floating point values by 1000 and converted them to integers (for
input to Clipper). My solution is not quite exact, but close enough at our
level. It also has only 2 branches and arithmetic operations without new
object allocation (I think).
I do not know how much slower your implementation would be, but I'd expect it
to be so. I'd be interested in timings between the two if you happen to run
them.
Original comment by buckyballreaction
on 30 Apr 2014 at 7:05