New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Polygons which have a vertex exactly "west" of the origin where that vertex does not have any clockwise walls can produce some artifacts due to completing the sweep incorrectly. #6317
Comments
Originally in GitLab by @caewok Two initial observations:
|
Originally in GitLab by @caewok Took a run through the In // Case 3 - If there are no counter-clockwise edges we must be beginning traversal down a new edge
// empty -> edge
// empty -> limited
if ( !activeEdges.size || !nccw ) {
return this._beginNewEdge(ray, result, activeEdges, isBinding);
} Instead, what should happen is that there should be one active edge, and vertex 1 should be processed in I strongly suspect the problem lies with _initializeActiveEdges() {
const rMin = this.config.rMin;
const edges = new Set();
for ( let edge of this.edges ) {
const x = foundry.utils.lineSegmentIntersects(rMin.A, rMin.B, edge.A, edge.B);
if ( x ) edges.add(edge);
}
return edges;
} The
This tells me that as the radius gets larger, numerical approximations are cropping up and causing the segment intersection test to fail. This is likely only a problem because we need this intersection test to be consistent with the vertices sort and the identification of ccw/cw edges. In other words, if Foundry thinks the ray due west does not intersect this first vertex, that would be fine but for the fact that the sort placed it first. This inconsistency likely is causing the error. It shows up for larger radius values because a larger radius increases the distance between the ray and the vertex if the ray is slightly off-angle due to a numerical approximation like 1099.9999 instead of 1100. Possible solutions: (1) see if a robust ccw test for the sort and this intersects test makes a difference; (2) make the intersects test more lenient, maybe by also testing for closeness to an edge vertex; (3) ?? somehow tie the initialize active edges more strongly to the sort, to resolve the inconsistency. |
Originally in GitLab by @dev7355608 I know very little about this stuff, but I observed that the collisions of the first/westwards ray are not ordered correctly. Not sure why |
Originally in GitLab by @caewok A simple fix would be to add the ccw edges for the first vertex to |
Originally in GitLab by @dev7355608
|
Originally in GitLab by @caewok If you want a more permissive intersection test, here is one option: /**
* Quickly test whether the line segment AB nearly intersects with the line segment CD,
* meaning that they are within ± √2 / 2 of one another
* This method does not determine the point of intersection, for that use lineLineIntersection
* @memberof helpers
*
* @param {Point} a The first endpoint of segment AB
* @param {Point} b The second endpoint of segment AB
* @param {Point} c The first endpoint of segment CD
* @param {Point} d The second endpoint of segment CD
*
* @returns {boolean} Do the line segments intersect?
*/
function lineSegmentNearlyIntersects(a, b, c, d) {
const xab = (nearOrient2d(a, b, c) * nearOrient2d(a, b, d)) <= 0;
const xcd = (nearOrient2d(c, d, a) * nearOrient2d(c, d, b)) <= 0;
return xab && xcd;
}
/**
* Is the point counterclockwise, clockwise, or colinear w/r/t the ray a,b
* Consider coordinates within ± √2 / 2 from the line as colinear
* (√2 / 2 is half the diagonal distance between two pixels on a grid, where the
* pixels are at opposite corners of a grid square.)
* @param {Point} a The first endpoint of segment AB
* @param {Point} b The second endpoint of segment AB
* @param {Point} c The first endpoint of segment CD
*/
function nearOrient2d(a, b, c) {
// p may be close enough to the line to be colinear
// orient2d returns ~ double the area of the triangle formed by the three points.
// in the base case, orient2d for a line 1,1 --> 1,0 and a point 1 - √2 / 2, 0 returns
// area of √2 / 2.
// orient 2d for a line 1,2 --> 1,0 with point 1 - √2 / 2, 0 returns √2 / 2 * 2
// I.e., a given point would be nearly colinear if orient2d <= √2 /2 * line distance
// Can square both sides, to compare orient2d ^ 2 <= 0.5 * line distance ^2
const orientation = orient2dFast(a, b, c);
const orientation2 = Math.pow(orientation, 2);
const dx = b.x - a.x;
const dy = b.y - a.y
const dist2 = Math.pow(dx, 2) + Math.pow(dy, 2);
const cutoff = 0.5 * dist2;
if(orientation2 < cutoff) return 0;
return orientation;
} The idea here is that if your coordinates are within 1 pixel, you want them treated as equal---"rounding" them to the nearest pixel. Imagine a grid square, where 0,0 is a pixel and 0,1 is another pixel. 0,0.5 could be equivalent to either pixel, so a line that goes through 0,0.5 should "intersect" a line going through 0,1 or 0,0. Now, it might be sufficient to use 0.5, but the above actually does the math for when you have 0,0 and want, say, 0.5, 0.5 to be equivalent. It turns out the math for considering the diagonal case is fairly simple, as the √2 /2 ends up being squared to 0.5.
|
Originally in GitLab by @dev7355608 If the ray start-/endpoints were integer, we wouldn't need those special intersection tests. But I'm not sure if the ray endpoints cannot be rounded for an important reason, so this may be irrelevant. // Assume a, b, c, and d have less than 2^25 and greater than or equal to -2^25 integer coordinates.
// 100% accurate.
function lineSegmentIntersects(a, b, c, d) {
const xab = (orient2dFast(a, b, c) * orient2dFast(a, b, d)) <= 0;
const xcd = (orient2dFast(c, d, a) * orient2dFast(c, d, b)) <= 0;
return xab && xcd;
}
// The intersection point may not be the most accurate, but whether we get an intersection is 100% accurate.
function lineLineIntersection(a, b, c, d) { /* ... */ }
// With a nonzero epsilon this may actually return false-positives; it is 100% accurate if epsilon is zero.
function lineSegmentIntersection(a, b, c, d, epsilon=1e-8) { /* ... */ }
// Here aInside/bInside-checks would be accurate if center.x/center.y are integers, epsilon is zero, and we would get passed radius squared instead of radius and radius squared is precise.
// Not sure about the accuracy of quadraticIntersection given integers.
function lineCircleIntersection(a, b, center, radius, epsilon=1e-8) { /* ... */ } |
Originally in GitLab by @caewok The ray start points should be the origin, which is usually (always?) integer coordinates. The ray end points will not always be integers and cannot always be integers. However, for the initial ray in the non-limited-angle case, the ray is always due west, I believe. This due west ray should have integer coordinates at the endpoint. (I am not sure why it currently doesn't always, but seems like something is causing it to be slightly off.) |
Originally in GitLab by @dev7355608 The origin is sometimes noninteger to break ties with walls. The clockwise polygon only is guaranteed to work correctly up to a scene size of (2^16-1)x(2^16-1) by design (because of PolygonVertex.key). So there are still 9 bit of the 25 bits unused, well 2 bits we need to reserve so that ray startpoint + maxR doesn't overflow, I think. That leaves us with 7 bits for sub-pixel accuracy. Do we need more accuracy than 2 decimal places? |
I appreciate the careful thought going into this discussion, and I am interested in long-term opportunities to improve the algorithm. In the (very) short term for v9 stable release I'm going to look for something that is more of a workaround - we'll see if I can find a simple band-aid. |
Originally in GitLab by @caewok Makes sense, Andrew. Regarding non-integer coordinates, I don't think the user needs more than two decimals to place walls. Particularly if you are limiting scene size by design. Indeed, we are already assuming integer coordinates for walls. So that leaves intersections with:
|
Too risky to try and deep-dive/address for the stable release today. We hope to address this during our V9 maintenance cycle. |
Need a bit more time on this one. |
Originally in GitLab by @Feu-Secret marked this issue as related to #6339 |
The text was updated successfully, but these errors were encountered: