How does one compute the area of intersection between a triangle (specified as three (X,Y) pairs) and a circle (X,Y,R)? I've done some searching to no avail. This is for work, not school. :)
It would look something like this in C#:
struct { PointF vert[3]; } Triangle;
struct { PointF center; float radius; } Circle;
// returns the area of intersection, e.g.:
// if the circle contains the triangle, return area of triangle
// if the triangle contains the circle, return area of circle
// if partial intersection, figure that out
// if no intersection, return 0
double AreaOfIntersection(Triangle t, Circle c)
{
...
}
First I will remind us how to find the area of a polygon. Once we have done this, the algorithm to find the intersection between a polygon and a circle should be easy to understand.
How to find the area of a polygon
Let's look at the case of a triangle, because all the essential logic appears there. Let's assume we have a triangle with vertices (x1,y1), (x2,y2), and (x3,y3) as you go around the triangle counter-clockwise, as shown in the following figure:
Then you can compute the area by the formula
A=(x1 y2 + x2 y3 + x3 y1 - x2y1- x3 y2 - x1y3)/2.
To see why this formula works, let's rearrange it so it is in the form
A=(x1 y2 - x2 y1)/2 + (x2 y3 - x3 y2)/2 + (x3 y1 - x1y3 )/2.
Now the first term is the following area, which is positive in our case:
If it isn't clear that the area of the green region is indeed (x1 y2 - x2 y1)/2, then read this.
The second term is this area, which is again positive:
And the third area is shown in the following figure. This time the area is negative
Adding these three up we get the following picture
We see that the green area that was outside the triangle is cancelled by the red area, so that the net area is just the area of the triangle, and this shows why our formula was true in this case.
What I said above was the intuitive explanation as to why the area formula was correct. A more rigorous explanation would be to observe that when we calculate the area from an edge, the area we get is the same area we would get from integration r^2dθ/2, so we are effectively integrating r^2dθ/2 around the boundary of the polygon, and by stokes theorem, this gives the same result as integrating rdrdθ over the region bounded the polygon. Since integrating rdrdθ over the region bounded by the polygon gives the area, we conclude that our procedure must correctly give the area.
Area of the intersection of a circle with a polygon
Now let's discuss how to find the area of the intersection of a circle of radius R with a polygon as show in the following figure:
We are interested in find the area of the green region. We may, just as in the case of the single polygon, break our calculation into finding an area for each side of the polygon, and then add those areas up.
Our first area will look like:
The second area will look like
And the third area will be
Again, the first two areas are positive in our case while the third one will be negative. Hopefully the cancellations will work out so that the net area is indeed the area we are interested in. Let's see.
Indeed the sum of the areas will be area we are interested in.
Again, we can give a more rigorous explanation of why this works. Let I be the region defined by the intersection and let P be the polygon. Then from the previous discussion, we know that we want to computer the integral of r^2dθ/2 around the boundary of I. However, this difficult to do because it requires finding the intersection.
Instead we did an integral over the polygon. We integrated max(r,R)^2 dθ/2 over the boundary of the polygon. To see why this gives the right answer, let's define a function π which takes a point in polar coordinates (r,θ) to the point (max(r,R),θ). It shouldn't be confusing to refer to the the coordinate functions of π(r)=max(r,R) and π(θ)=θ. Then what we did was to integrate π(r)^2 dθ/2 over the boundary of the polygon.
On the other hand since π(θ)=θ, this is the same as integrating π(r)^2 dπ(θ)/2 over the boundary of the polygon.
Now doing a change of variable, we find that we would get the same answer if we integrated r^2 dθ/2 over the boundary of π(P), where π(P) is the image of P under π.
Using Stokes theorem again we know that integrating r^2 dθ/2 over the boundary of π(P) gives us the area of π(P). In other words it gives the same answer as integrating dxdy over π(P).
Using a change of variable again, we know that integrating dxdy over π(P) is the same as integrating Jdxdy over P, where J is the jacobian of π.
Now we can split the integral of Jdxdy into two regions: the part in the circle and the part outside the circle. Now π leaves points in the circle alone so J=1 there, so the contribution from this part of P is the area of the part of P that lies in the circle i.e., the area of the intersection. The second region is the region outside the circle. There J=0 since π collapses this part down to the boundary of the circle.
Thus what we compute is indeed the area of the intersection.
Now that we are relatively sure we know conceptually how to find the area, let's talk more specifically about how to compute the contribution from a single segment. Let's start by looking at a segment in what I will call "standard geometry". It is shown below.
In standard geometry, the edge goes horizontally from left to right. It is described by three numbers: xi, the x-coordinate where the edge starts, xf, the x-coordinate where the edge ends, and y, the y coordinate of the edge.
Now we see that if |y| < R, as in the figure, then the edge will intersect the circle at the points (-xint,y) and (xint,y) where xint = (R^2-y^2)^(1/2). Then the area we need to calculate is broken up into three pieces labelled in the figure. To get the areas of regions 1 and 3, we can use arctan to get the angles of the various points and then equate the area to R^2 Δθ/2. So for example we would set θi = atan2(y,xi) and θl = atan2(y,-xint). Then the area of region one is R^2 (θl-θi)/2. We can obtain the area of region 3 similarly.
The area of region 2 is just the area of a triangle. However, we must be careful about sign. We want the area shown to be positive so we will say the area is -(xint - (-xint))y/2.
Another thing to keep in mind is that in general, xi does not have to be less than -xint and xf does not have to be greater than xint.
The other case to consider is |y| > R. This case is simpler, because there is only one piece which is similar to region 1 in the figure.
Now that we know how to compute the area from an edge in standard geometry, the only thing left to do is describe how to transform any edge into standard geometry.
But this just a simple change of coordinates. Given some with initial vertex vi and final vertex vf, the new x unit vector will be the unit vector pointing from vi to vf. Then xi is just the displacement of vi from the center of the circle dotted into x, and xf is just xi plus the distance between vi and vf. Meanwhile y is given by the wedge product of x with the displacement of vi from the center of the circle.
Code
That completes the description of the algorithm, now it is time to write some code. I will use java.
First off, since we are working with circles, we should have a circle class
public class Circle {
final Point2D center;
final double radius;
public Circle(double x, double y, double radius) {
center = new Point2D.Double(x, y);
this.radius = radius;
}
public Circle(Point2D.Double center, double radius) {
this(center.getX(), center.getY(), radius);
}
public Point2D getCenter() {
return new Point2D.Double(getCenterX(), getCenterY());
}
public double getCenterX() {
return center.getX();
}
public double getCenterY() {
return center.getY();
}
public double getRadius() {
return radius;
}
}
For polygons, I will use java's Shape class. Shapes have a PathIterator that I can use to iterate through the edges of the polygon.
Now for the actual work. I will separate the logic of iterating through the edges, putting the edges in standard geometry etc, from the logic of computing the area once this is done. The reason for this is that you may in the future want to compute something else besides or in addition to the area and you want to be able to reuse the code having to deal with iterating through the edges.
So I have a generic class which computes some property of class T about our polygon circle intersection.
public abstract class CircleShapeIntersectionFinder<T> {
It has three static methods that just help compute geometry:
private static double[] displacment2D(final double[] initialPoint, final double[] finalPoint) {
return new double[]{finalPoint[0] - initialPoint[0], finalPoint[1] - initialPoint[1]};
}
private static double wedgeProduct2D(final double[] firstFactor, final double[] secondFactor) {
return firstFactor[0] * secondFactor[1] - firstFactor[1] * secondFactor[0];
}
static private double dotProduct2D(final double[] firstFactor, final double[] secondFactor) {
return firstFactor[0] * secondFactor[0] + firstFactor[1] * secondFactor[1];
}
There are two instance fields, a Circle which just keeps a copy of the circle, and the currentSquareRadius, which keeps a copy of the square radius. This may seem odd, but the class I am using is actually equipped to find the areas of a whole collection of circle-polygon intersections. That is why I am referring to one of the circles as "current".
private Circle currentCircle;
private double currentSquareRadius;
Next comes the method for computing what we want to compute:
public final T computeValue(Circle circle, Shape shape) {
initialize();
processCircleShape(circle, shape);
return getValue();
}
initialize() and getValue() are abstract. initialize() would set the variable that is keeping a total of the area to zero, and getValue() would just return the area. The definition for processCircleShape is
private void processCircleShape(Circle circle, final Shape cellBoundaryPolygon) {
initializeForNewCirclePrivate(circle);
if (cellBoundaryPolygon == null) {
return;
}
PathIterator boundaryPathIterator = cellBoundaryPolygon.getPathIterator(null);
double[] firstVertex = new double[2];
double[] oldVertex = new double[2];
double[] newVertex = new double[2];
int segmentType = boundaryPathIterator.currentSegment(firstVertex);
if (segmentType != PathIterator.SEG_MOVETO) {
throw new AssertionError();
}
System.arraycopy(firstVertex, 0, newVertex, 0, 2);
boundaryPathIterator.next();
System.arraycopy(newVertex, 0, oldVertex, 0, 2);
segmentType = boundaryPathIterator.currentSegment(newVertex);
while (segmentType != PathIterator.SEG_CLOSE) {
processSegment(oldVertex, newVertex);
boundaryPathIterator.next();
System.arraycopy(newVertex, 0, oldVertex, 0, 2);
segmentType = boundaryPathIterator.currentSegment(newVertex);
}
processSegment(newVertex, firstVertex);
}
Let's take a second to look at initializeForNewCirclePrivate quickly. This method just sets the instance fields and allows the derived class to store any property of the circle. Its definition is
private void initializeForNewCirclePrivate(Circle circle) {
currentCircle = circle;
currentSquareRadius = currentCircle.getRadius() * currentCircle.getRadius();
initializeForNewCircle(circle);
}
initializeForNewCircle is abstract and one implementation would be for it to store the circles radius to avoid having to do square roots. Anyway back to processCircleShape. After calling initializeForNewCirclePrivate, we check if the polygon is null (which I am interpreting as an empty polygon), and we return if it is null. In this case, our computed area would be zero. If the polygon is not null then we get the PathIterator of the polygon. The argument to the getPathIterator method I call is an affine transformation that can be applied to the path. I don't want to apply one though, so I just pass null.
Next I declare the double[]s that will keep track of the vertices. I must remember the first vertex because the PathIterator only gives me each vertex once, so I have to go back after it has given me the last vertex, and form an edge with this last vertex and the first vertex.
The currentSegment method on the next line puts the next vertex in its argument. It returns a code that tells you when it is out of vertices. This is why the control expression for my while loop is what it is.
Most of the rest of the code of this method is uninteresting logic related to iterating through the vertices. The important thing is that once per iteration of the while loop I call processSegment and then I call processSegment again at the end of the method to process the edge that connects the last vertex to the first vertex.
Let's look at the code for processSegment:
private void processSegment(double[] initialVertex, double[] finalVertex) {
double[] segmentDisplacement = displacment2D(initialVertex, finalVertex);
if (segmentDisplacement[0] == 0 && segmentDisplacement[1] == 0) {
return;
}
double segmentLength = Math.sqrt(dotProduct2D(segmentDisplacement, segmentDisplacement));
double[] centerToInitialDisplacement = new double[]{initialVertex[0] - getCurrentCircle().getCenterX(), initialVertex[1] - getCurrentCircle().getCenterY()};
final double leftX = dotProduct2D(centerToInitialDisplacement, segmentDisplacement) / segmentLength;
final double rightX = leftX + segmentLength;
final double y = wedgeProduct2D(segmentDisplacement, centerToInitialDisplacement) / segmentLength;
processSegmentStandardGeometry(leftX, rightX, y);
}
In this method I implement the steps to transform an edge into the standard geometry as described above. First I calculate segmentDisplacement, the displacement from the initial vertex to the final vertex. This defines the x axis of the standard geometry. I do an early return if this displacement is zero.
Next I calculate the length of the displacement, because this is necessary to get the x unit vector. Once I have this information, I calculate the displacement from the center of the circle to the initial vertex. The dot product of this with segmentDisplacement gives me leftX which I had been calling xi. Then rightX, which I had been calling xf, is just leftX + segmentLength. Finally I do the wedge product to get y as described above.
Now that I have transformed the problem into the standard geometry, it will be easy to deal with. That is what the processSegmentStandardGeometry method does. Let's look at the code
private void processSegmentStandardGeometry(double leftX, double rightX, double y) {
if (y * y > getCurrentSquareRadius()) {
processNonIntersectingRegion(leftX, rightX, y);
} else {
final double intersectionX = Math.sqrt(getCurrentSquareRadius() - y * y);
if (leftX < -intersectionX) {
final double leftRegionRightEndpoint = Math.min(-intersectionX, rightX);
processNonIntersectingRegion(leftX, leftRegionRightEndpoint, y);
}
if (intersectionX < rightX) {
final double rightRegionLeftEndpoint = Math.max(intersectionX, leftX);
processNonIntersectingRegion(rightRegionLeftEndpoint, rightX, y);
}
final double middleRegionLeftEndpoint = Math.max(-intersectionX, leftX);
final double middleRegionRightEndpoint = Math.min(intersectionX, rightX);
final double middleRegionLength = Math.max(middleRegionRightEndpoint - middleRegionLeftEndpoint, 0);
processIntersectingRegion(middleRegionLength, y);
}
}
The first if distinguishes the cases where y is small enough that the edge may intersect the circle. If y is big and there is no possibility of intersection, then I call the method to handle that case. Otherwise I handle the case where intersection is possible.
If intersection is possible, I calculate the x coordinate of intersection, intersectionX, and I divide the edge up into three portions, which correspond to regions 1, 2, and 3 of the standard geometry figure above. First I handle region 1.
To handle region 1, I check if leftX is indeed less than -intersectionX for otherwise there would be no region 1. If there is a region 1, then I need to know when it ends. It ends at the minimum of rightX and -intersectionX. After I have found these x-coordinates, I deal with this non-intersection region.
I do a similar thing to handle region 3.
For region 2, I have to do some logic to check that leftX and rightX do actually bracket some region in between -intersectionX and intersectionX. After finding the region, I only need the length of the region and y, so I pass these two numbers on to an abstract method which handles the region 2.
Now let's look at the code for processNonIntersectingRegion
private void processNonIntersectingRegion(double leftX, double rightX, double y) {
final double initialTheta = Math.atan2(y, leftX);
final double finalTheta = Math.atan2(y, rightX);
double deltaTheta = finalTheta - initialTheta;
if (deltaTheta < -Math.PI) {
deltaTheta += 2 * Math.PI;
} else if (deltaTheta > Math.PI) {
deltaTheta -= 2 * Math.PI;
}
processNonIntersectingRegion(deltaTheta);
}
I simply use atan2 to calculate the difference in angle between leftX and rightX. Then I add code to deal with the discontinuity in atan2, but this is probably unnecessary, because the discontinuity occurs either at 180 degrees or 0 degrees. Then I pass the difference in angle onto an abstract method. Lastly we just have abstract methods and getters:
protected abstract void initialize();
protected abstract void initializeForNewCircle(Circle circle);
protected abstract void processNonIntersectingRegion(double deltaTheta);
protected abstract void processIntersectingRegion(double length, double y);
protected abstract T getValue();
protected final Circle getCurrentCircle() {
return currentCircle;
}
protected final double getCurrentSquareRadius() {
return currentSquareRadius;
}
}
Now let's look at the extending class, CircleAreaFinder
public class CircleAreaFinder extends CircleShapeIntersectionFinder<Double> {
public static double findAreaOfCircle(Circle circle, Shape shape) {
CircleAreaFinder circleAreaFinder = new CircleAreaFinder();
return circleAreaFinder.computeValue(circle, shape);
}
double area;
#Override
protected void initialize() {
area = 0;
}
#Override
protected void processNonIntersectingRegion(double deltaTheta) {
area += getCurrentSquareRadius() * deltaTheta / 2;
}
#Override
protected void processIntersectingRegion(double length, double y) {
area -= length * y / 2;
}
#Override
protected Double getValue() {
return area;
}
#Override
protected void initializeForNewCircle(Circle circle) {
}
}
It has a field area to keep track of the area. initialize sets area to zero, as expected. When we process a non intersecting edge, we increment the area by R^2 Δθ/2 as we concluded we should above. For an intersecting edge, we decrement the area by y*length/2. This was so that negative values for y correspond to positive areas, as we decided they should.
Now the neat thing is if we want to keep track of the perimeter we don't have to do that much more work. I defined an AreaPerimeter class:
public class AreaPerimeter {
final double area;
final double perimeter;
public AreaPerimeter(double area, double perimeter) {
this.area = area;
this.perimeter = perimeter;
}
public double getArea() {
return area;
}
public double getPerimeter() {
return perimeter;
}
}
and now we just need to extend our abstract class again using AreaPerimeter as the type.
public class CircleAreaPerimeterFinder extends CircleShapeIntersectionFinder<AreaPerimeter> {
public static AreaPerimeter findAreaPerimeterOfCircle(Circle circle, Shape shape) {
CircleAreaPerimeterFinder circleAreaPerimeterFinder = new CircleAreaPerimeterFinder();
return circleAreaPerimeterFinder.computeValue(circle, shape);
}
double perimeter;
double radius;
CircleAreaFinder circleAreaFinder;
#Override
protected void initialize() {
perimeter = 0;
circleAreaFinder = new CircleAreaFinder();
}
#Override
protected void initializeForNewCircle(Circle circle) {
radius = Math.sqrt(getCurrentSquareRadius());
}
#Override
protected void processNonIntersectingRegion(double deltaTheta) {
perimeter += deltaTheta * radius;
circleAreaFinder.processNonIntersectingRegion(deltaTheta);
}
#Override
protected void processIntersectingRegion(double length, double y) {
perimeter += Math.abs(length);
circleAreaFinder.processIntersectingRegion(length, y);
}
#Override
protected AreaPerimeter getValue() {
return new AreaPerimeter(circleAreaFinder.getValue(), perimeter);
}
}
We have a variable perimeter to keep track of the perimeter, we remember the value of the radius to avoid have to call Math.sqrt a lot, and we delegate the calculation of the area to our CircleAreaFinder. We can see that the formulas for the perimeter are easy.
For reference here is the full code of CircleShapeIntersectionFinder
private static double[] displacment2D(final double[] initialPoint, final double[] finalPoint) {
return new double[]{finalPoint[0] - initialPoint[0], finalPoint[1] - initialPoint[1]};
}
private static double wedgeProduct2D(final double[] firstFactor, final double[] secondFactor) {
return firstFactor[0] * secondFactor[1] - firstFactor[1] * secondFactor[0];
}
static private double dotProduct2D(final double[] firstFactor, final double[] secondFactor) {
return firstFactor[0] * secondFactor[0] + firstFactor[1] * secondFactor[1];
}
private Circle currentCircle;
private double currentSquareRadius;
public final T computeValue(Circle circle, Shape shape) {
initialize();
processCircleShape(circle, shape);
return getValue();
}
private void processCircleShape(Circle circle, final Shape cellBoundaryPolygon) {
initializeForNewCirclePrivate(circle);
if (cellBoundaryPolygon == null) {
return;
}
PathIterator boundaryPathIterator = cellBoundaryPolygon.getPathIterator(null);
double[] firstVertex = new double[2];
double[] oldVertex = new double[2];
double[] newVertex = new double[2];
int segmentType = boundaryPathIterator.currentSegment(firstVertex);
if (segmentType != PathIterator.SEG_MOVETO) {
throw new AssertionError();
}
System.arraycopy(firstVertex, 0, newVertex, 0, 2);
boundaryPathIterator.next();
System.arraycopy(newVertex, 0, oldVertex, 0, 2);
segmentType = boundaryPathIterator.currentSegment(newVertex);
while (segmentType != PathIterator.SEG_CLOSE) {
processSegment(oldVertex, newVertex);
boundaryPathIterator.next();
System.arraycopy(newVertex, 0, oldVertex, 0, 2);
segmentType = boundaryPathIterator.currentSegment(newVertex);
}
processSegment(newVertex, firstVertex);
}
private void initializeForNewCirclePrivate(Circle circle) {
currentCircle = circle;
currentSquareRadius = currentCircle.getRadius() * currentCircle.getRadius();
initializeForNewCircle(circle);
}
private void processSegment(double[] initialVertex, double[] finalVertex) {
double[] segmentDisplacement = displacment2D(initialVertex, finalVertex);
if (segmentDisplacement[0] == 0 && segmentDisplacement[1] == 0) {
return;
}
double segmentLength = Math.sqrt(dotProduct2D(segmentDisplacement, segmentDisplacement));
double[] centerToInitialDisplacement = new double[]{initialVertex[0] - getCurrentCircle().getCenterX(), initialVertex[1] - getCurrentCircle().getCenterY()};
final double leftX = dotProduct2D(centerToInitialDisplacement, segmentDisplacement) / segmentLength;
final double rightX = leftX + segmentLength;
final double y = wedgeProduct2D(segmentDisplacement, centerToInitialDisplacement) / segmentLength;
processSegmentStandardGeometry(leftX, rightX, y);
}
private void processSegmentStandardGeometry(double leftX, double rightX, double y) {
if (y * y > getCurrentSquareRadius()) {
processNonIntersectingRegion(leftX, rightX, y);
} else {
final double intersectionX = Math.sqrt(getCurrentSquareRadius() - y * y);
if (leftX < -intersectionX) {
final double leftRegionRightEndpoint = Math.min(-intersectionX, rightX);
processNonIntersectingRegion(leftX, leftRegionRightEndpoint, y);
}
if (intersectionX < rightX) {
final double rightRegionLeftEndpoint = Math.max(intersectionX, leftX);
processNonIntersectingRegion(rightRegionLeftEndpoint, rightX, y);
}
final double middleRegionLeftEndpoint = Math.max(-intersectionX, leftX);
final double middleRegionRightEndpoint = Math.min(intersectionX, rightX);
final double middleRegionLength = Math.max(middleRegionRightEndpoint - middleRegionLeftEndpoint, 0);
processIntersectingRegion(middleRegionLength, y);
}
}
private void processNonIntersectingRegion(double leftX, double rightX, double y) {
final double initialTheta = Math.atan2(y, leftX);
final double finalTheta = Math.atan2(y, rightX);
double deltaTheta = finalTheta - initialTheta;
if (deltaTheta < -Math.PI) {
deltaTheta += 2 * Math.PI;
} else if (deltaTheta > Math.PI) {
deltaTheta -= 2 * Math.PI;
}
processNonIntersectingRegion(deltaTheta);
}
protected abstract void initialize();
protected abstract void initializeForNewCircle(Circle circle);
protected abstract void processNonIntersectingRegion(double deltaTheta);
protected abstract void processIntersectingRegion(double length, double y);
protected abstract T getValue();
protected final Circle getCurrentCircle() {
return currentCircle;
}
protected final double getCurrentSquareRadius() {
return currentSquareRadius;
}
Anyway, that is my description of the algorithm. I think it is nice because it is exact and there aren't really that many cases to check.
If you want an exact solution (or at least as exact as you can get using floating-point arithmetic) then this is going to involve a lot of legwork, because there are so many cases to consider.
I count nine different cases (categorized in the figure below by the number of vertices of the triangle inside the circle, and the number of edges of the triangle that intersect or are contained in the circle):
(However, this kind of enumeration of geometric cases is well known to be tricky, and it wouldn't surprise me at all if I missed one or two!)
So the approach is:
Determine for each vertex of the triangle if it's inside the circle. I'm going to assume you know how to do that.
Determine for each edge of the triangle if it intersects the circle. (I wrote up one method here, or see any computational geometry book.) You'll need to compute the point or points of intersection (if any) for use in step 4.
Determine which of the nine cases you have.
Compute the area of the intersection. Cases 1, 2, and 9 are easy. In the remaining six cases I've drawn dashed lines to show how to partition the area of intersection into triangles and circular segments based on the original vertices of the triangle, and on the points of intersection you computed in step 2.
This algorithm is going to be rather delicate and prone to errors that affect only one of the cases, so make sure you have test cases that cover all nine cases (and I suggest permuting the vertices of the test triangles too). Pay particular attention to cases in which one of the vertices of the triangle is on the edge of the circle.
If you don't need an exact solution, then rasterizing the figures and counting the pixels in the intersection (as suggested by a couple of other respondents) seems like a much easier approach to code, and correspondingly less prone to errors.
I'm almost a year and a half late, but I thought maybe people will be interested in code here that I wrote which I think does this correctly. Look in function IntersectionArea near the bottom. The general approach is to pick off the convex polygon circumscribed by the circle, and then deal with the little circular caps.
Assuming you're talking integer pixels, not real, the naive implementation would be to loop through every pixel of the triangle and check the distance from the circle's center against its radius.
It's not a cute formula, or particularly fast, but it does get the job done.
try computational geometry
Note: this is not a trivial problem, I hope it's not homework ;-)
If you have a GPU at your disposal, you could use this technique for obtaining a pixel count of the intersection..
I think you shouldn't approximate circle as some set of triangles, instead of that you can approximate it's shape with a polygon.
The naive algorithm can look like:
Convert you circle to polygon with some desired number of vertices.
Calculate the intersection of two polygons (converted circle and a triangle).
Calculate square of that intersection.
You can optimize this algorithm by combining step 2 and step 3 into single function.
Read this links:
Area of convex polygon
Intersection of convex polygons
Since your shapes are convex, you can use Monte Carlo area estimation.
Draw a box around the circle and triangle.
Choose random points in the box and keep a count of how many fall in the circle, and how many fall in both the circle and triangle.
Area of Intersection ≅ Area of circle * # points in circle and triangle / # points in circle
Stop choosing points when the estimated area doesn't change by more than a certain amount over a certain number of rounds, or just choose a fixed number of points based on the area of the box. The area estimate should converge pretty fast unless one of your shapes has very little area.
Note: Here's how you determine if a point is in a triangle: Barycentric coordinates
How exact do you need to be? If you can approximate the circle with simpler shapes, you can simplify the problem. It wouldn't be hard to model a circle as a set of very narrow triangles meeting at the center, for example.
My first instinct would be to transform everything so that the circle is centered on origin, trans the triangle to polar coordinates, and solve for the intersection (or encompassment) of the triangle with the circle. I haven't actually worked it through on paper yet though so that's only a hunch.
If just one of the triangle's line segments intersects the circle, the pure math solution isn't too hard. Once you know when the two points of intersection are, you can use the distance formula to find the chord length.
According to these equations:
ϑ = 2 sin⁻¹(0.5 c / r)
A = 0.5 r² (ϑ - sin(ϑ))
where c is the chord length, r is the radius, ϑ becomes the angle through the center, and A is the area. Note that this solution breaks if more than half the circle is cut off.
It's probably not worth the effort if you just need an approximation, since it makes several assumptions about what the actual intersection looks like.
Related
So basically I'm trying to make a simple open gl 3D graphics engine using my own linear algebra to make projection and transformation matrices. OpenGL has a class called glUniformMatrix4fv() which I use to pass the matrices as a float[].
Here is my "Matrix" class to construct a float[] for that openGl method:
private float[] m= {
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
};
public Matrix() {}
public Matrix(float[] m) {
this.m=m;
}
//gets value at x,y coords of matrix
public float getValue(int x,int y) {
return m[y*4 + x];
}
//sets value of x,y coord to n
public void setValue(int x,int y,float n) {
m[y*4 + x]=n;
}
To construct a transformation for object translation and rotation I first create translation Matrix (s is for scale). Also a vertex is basically just a size 4 float array I have my Vector/vertex info in:
public Matrix createTranslationMatrix(Vertex pos,float s) {
Matrix m=new Matrix();
m.setValue(0,0,s);
m.setValue(1,1,s);
m.setValue(2,2,s);
m.setValue(3,0,pos.getValue(0));
m.setValue(3,1,pos.getValue(1));
m.setValue(3,2,pos.getValue(2));
return m;
}
Then I create a rotation matrix which is a combo of x, y, and z rotation of object around origin
public Matrix createRotationMatrix(Vertex rot) {
//if rotation is screwed up maybe mess around with order of these :)
Matrix rotX=createRotationMatrixX(rot.getValue(0));
Matrix rotY=createRotationMatrixY(rot.getValue(1));
Matrix rotZ=createRotationMatrixZ(rot.getValue(2));
Matrix returnValue=multiply(rotX,rotY);
returnValue=multiply(returnValue,rotZ);
return returnValue;
}
private Matrix createRotationMatrixX(float num) {
float n=num;
n=(float)Math.toRadians(n);
Matrix rot=new Matrix();
rot.setValue(1, 1, (float)Math.cos(n));
rot.setValue(1, 2, (float)Math.sin(n));
rot.setValue(2, 1, (float)-Math.sin(n));
rot.setValue(2, 2, (float)Math.cos(n));
return rot;
}
//rotation mat Y
private Matrix createRotationMatrixY(float num) {
float n=num;
n=(float)Math.toRadians(n);
Matrix rot=new Matrix();
rot.setValue(0, 0, (float)Math.cos(n));
rot.setValue(0, 2, (float)-Math.sin(n));
rot.setValue(2, 0, (float)Math.sin(n));
rot.setValue(2, 2, (float)Math.cos(n));
return rot;
}
//rotation mat Z
private Matrix createRotationMatrixZ(float num) {
float n=num;
n=(float)Math.toRadians(n);
Matrix rot=new Matrix();
rot.setValue(0, 0, (float)Math.cos(n));
rot.setValue(0, 1, (float)Math.sin(n));
rot.setValue(1, 0, (float)-Math.sin(n));
rot.setValue(1, 1, (float)Math.cos(n));
return rot;
}
I combine the translation and create my objectTransform float[] using a matrix with multiply(rotationMat,translationMat):
public Matrix multiply(Matrix a, Matrix b){
Matrix m=new Matrix();
for(int y=0;y<4;y++) {
for(int x=0;x<4;x++) {
//if this doesn't work maybe try switching x and y around?
m.setValue(x,y,a.getValue(x,0)*b.getValue(0,y) + a.getValue(x,1)*b.getValue(1,y) + a.getValue(x,2)*b.getValue(2,y) + a.getValue(x,3)*b.getValue(3, y));
}
}
return m;
}
And my code for my worldTransorm is defined from by combining a transformation with negative values for position and rotation (so it moves vertex and rotates opposite from camera position and rotation), then combinging rotation and transformation like so multiply(translationMat,rotationMat) , so it theoretically moves opposite camera pos, THEN rotates opposite camera rotation.
then I create my projection using this function:
public Matrix createProjectionMatrix(float fov, float aspectRatio, float near, float far) {
float fovRad=1/(float)Math.tan(Math.toRadians(fov*.5));
Matrix projection=new Matrix(base);
projection.setValue(0,0,aspectRatio*fovRad);
projection.setValue(1,1,fovRad);
projection.setValue(2,2,far/(far-near));
projection.setValue(2,3,(-far*near)/(far-near));
projection.setValue(3,2,1);
projection.setValue(3,3,0);
return projection;
}
I combine my projection , worldTransform, and objectTransform with my Vec3 position (vector with mesh coordinates I import). These are all multiplied together in my openGL shader class like so:
gl_Position=projection * worldTransform * objectTransform * vec4(position,1);
Write now if I back my camera up by 3, rotate it around with hopes of finding the "triangle" mesh I made
float[] verts= {
//top left tri
-.5f,-.5f,0,
0,.5f,0,
.5f,-.5f,0,
};
Then I get a really small pixel moving really fast accross my screen from top to bottom. I also have the object spinning, but that (if my code worked properly) shouldn't be an issue, but if I don't have the object spinning, then I don't see any pixel at all. So my thinking is the object transformation is applying like the world transormation should be working, moving the vertex by "translation" then rotating it, or the triangle is really small and not scaled properly (do I have to offset it somehow?), but then it shouldn't be flying off the screen repeatedly as if its rotating around the camera. I've tried switching multiplication of translation and rotation for both types of transforms, but either the triangle doesn't appear at all or I just see a teensy tiny little pixel, almost orbitting the camera at high speeds (when I should see just the triangle and camera rotating seperately)
I know its a lot to ask but what am I doing wrong? Do I need to transpose something? Is my projection matrix out of wack? I feel like everything should be right :(
Lets say we get rectangles in form of 4 points: their (x1, y1), ..., (x4, y4)
We want to calculate the total area they cover. We want to count the total area, if more rectangles overlap, we count this area only once.
I'm not really looking for finished solution, pseudocode or some links to algorithms and data structures useful here would be appreciated.
the form of rectangles is: given by three integers: left position, right position and height. for example:
L:0 R:2 H:2
L:1 R:3 H:3
L:-1 R:4 H:1
total area would be: 10
the max value for x axis is from -1e9 to 1e9, starts at x=L and ends at x=R
y cannot be lower than 0 and always starts at y=0 and ends at y=H
Let's assume that your rectangles have integer coordinates in a very small range, say 0 to 10. Then an easy approach is to create a grid and paint the rectangles onto it:
The occupied "pixels" could be stored in a set or they could be set bits in a bitmap. When rectangles overlap, the intersection is just marked as occupied again and so contributes only once the the area. The area is the number of occupied cells.
For large dimensions, the data structures would become too big. Also, painting a rectangle that is several million pixels wide would be slow.
But the technique can still be applied when we use compressed coordinates. Your grid then has only the coordinates that are actual coordinates of the rectangles. The cells have variable widths and heights. The number of cells depends on the number of rectangles, but it is independent of the minimum and maximum coordinates:
The algorithm would look like this:
Find all unique x coordinates. Sort them in ascending order and store (coordinate, index) pairs in a dictionary for easy look-up.
Do the same for the y coordinates.
Paint the rectangles: Find the indices of x and y ranges and occupy the cells between them.
Calculate the area by summing the area of all occupied cells.
These rectangles have their base at y=0? I'm assuming that's true. So these are like buildings in a city viewed from a distance. You're trying to trace the skyline.
Store the rectangles in one array so you can use the array indices as unique IDs. Represent each left and right rectangle edge as an "event" that includes the ID for the rectangle it belongs to and the x-coordinate of the corresponding edge. Put all the events in a list EL and sort by x-coordinate. Finally you'll need a dynamically sorted set (e.g. a Java TreeSet) of rectangle IDs sorted by corresponding rectangle height descending. It's called SL, the "sweep line". Due the way it's sorted, SL.first is always the ID of the highest rectangle currently referenced by the SL.
Now you can draw the outline of the collection of rectangles as follows:
SL = <empty> // sweep line
x0 = EL.first.left // leftmost x among all rectangle edges
lastX = x0
for each event E in EL // process events left-to-right
Let y0 = if SL.isEmpty then 0 else SL.first.height // current y
if E.ID in SL // event in SL means sweep line is at rectangle's right edge
remove E.ID from SL
else // event means sweep line is a new rectangle's left edge
add E.ID to SL
Let y1 = if SL.isEmpty then 0 else SL.first.height // new y
if y1 != y0
output line seg (lastX, y0) -> (E.x, y0)
output line seg (E.x, y0) -> (E.x, y1)
lastX = E.x
output final line seg (lastX, 0) -> (x0, 0)
Because this sounds like homework or maybe an interview question, I'll let you revise this algorithm to provide the area of the swept-out shape rather than drawing its edge.
Addition
Just for fun:
import java.util.ArrayList;
import static java.lang.Integer.compare;
import static java.util.Arrays.stream;
import static java.util.Collections.sort;
import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
class SkyLine {
static class Rectangle {
final int left;
final int right;
final int height;
Rectangle(int left, int right, int height) {
this.left = left;
this.right = right;
this.height = height;
}
}
static class Event implements Comparable<Event> {
final int x;
final int id;
public Event(int x, int id) {
this.x = x;
this.id = id;
}
#Override
public int compareTo(Event e) { return compare(x, e.x); }
}
final List<Rectangle> rectangles = new ArrayList<>();
final Comparator byHeightDescending =
(Comparator<Integer>) (Integer a, Integer b) ->
compare(rectangles.get(b).height, rectangles.get(a).height);
final SortedSet<Integer> scanLine = new TreeSet<>(byHeightDescending);
final List<Event> events = new ArrayList<>();
SkyLine(Rectangle [] data) {
stream(data).forEach(rectangles::add);
int id = 0;
for (Rectangle r : rectangles) {
events.add(new Event(r.left, id));
events.add(new Event(r.right, id));
++id;
}
sort(events);
}
int area() {
int area = 0;
Event ePrev = null;
for (Event e : events) {
if (ePrev != null) area += (e.x - ePrev.x) * rectangles.get(scanLine.first()).height;
if (!scanLine.remove(e.id)) scanLine.add(e.id);
ePrev = e;
}
return area;
}
public static void main(String [] args) {
Rectangle [] data = {
new Rectangle(0, 2, 2),
new Rectangle(1, 3, 3),
new Rectangle(-1, 4, 1),
};
int area = new SkyLine(data).area();
System.out.println(area);
}
}
I am trying to create a shape similar to this, hexagons with 12 pentagons, at an arbitrary size.
(Image Source)
The only thing is, I have absolutely no idea what kind of code would be needed to generate it!
The goal is to be able to take a point in 3D space and convert it to a position coordinate on the grid, or vice versa and take a grid position and get the relevant vertices for drawing the mesh.
I don't even know how one would store the grid positions for this. Does each "triagle section" between 3 pentagons get their own set of 2D coordinates?
I will most likely be using C# for this, but I am more interested in which algorithms to use for this and an explanation of how they would work, rather than someone just giving me a piece of code.
The shape you have is one of so called "Goldberg polyhedra", is also a geodesic polyhedra.
The (rather elegant) algorithm to generate this (and many many more) can be succinctly encoded in something called a Conway Polyhedron Notation.
The construction is easy to follow step by step, you can click the images below to get a live preview.
The polyhedron you are looking for can be generated from an icosahedron -- Initialise a mesh with an icosahedron.
We apply a "Truncate" operation (Conway notation t) to the mesh (the sperical mapping of this one is a football).
We apply the "Dual" operator (Conway notation d).
We apply a "Truncate" operation again. At this point the recipe is tdtI (read from right!). You can already see where this is going.
Apply steps 3 & 4 repeatedly until you are satisfied.
For example below is the mesh for dtdtdtdtI.
This is quite easy to implement. I would suggest using a datastructure that makes it easy to traverse the neighbourhood give a vertex, edge etc. such as winged-edge or half-edge datastructures for your mesh. You only need to implement truncate and dual operators for the shape you are looking for.
First some analysis of the image in the question: the spherical triangle spanned by neighbouring pentagon centers seems to be equilateral. When five equilateral triangles meet in one corner and cover the whole sphere, this can only be the configuration induced by a icosahedron. So there are 12 pentagons and 20 patches of a triangular cutout of a hexongal mesh mapped to the sphere.
So this is a way to construct such a hexagonal grid on the sphere:
Create triangular cutout of hexagonal grid: a fixed triangle (I chose (-0.5,0),(0.5,0),(0,sqrt(3)/2) ) gets superimposed a hexagonal grid with desired resolution n s.t. the triangle corners coincide with hexagon centers, see the examples for n = 0,1,2,20:
Compute corners of icosahedron and define the 20 triangular faces of it (see code below). The corners of the icosahedron define the centers of the pentagons, the faces of the icosahedron define the patches of the mapped hexagonal grids. (The icosahedron gives the finest regular division of the sphere surface into triangles, i.e. a division into congruent equilateral triangles. Other such divisions can be derived from a tetrahedron or an octahedron; then at the corners of the triangles one will have triangles or squares, resp. Furthermore the fewer and bigger triangles would make the inevitable distortion in any mapping of a planar mesh onto a curved surface more visible. So choosing the icosahedron as a basis for the triangular patches helps minimizing the distortion of the hexagons.)
Map triangular cutout of hexagonal grid to spherical triangles corresponding to icosaeder faces: a double-slerp based on barycentric coordinates does the trick. Below is an illustration of the mapping of a triangular cutout of a hexagonal grid with resolution n = 10 onto one spherical triangle (defined by one face of an icosaeder), and an illustration of mapping the grid onto all these spherical triangles covering the whole sphere (different colors for different mappings):
Here is Python code to generate the corners (coordinates) and triangles (point indices) of an icosahedron:
from math import sin,cos,acos,sqrt,pi
s,c = 2/sqrt(5),1/sqrt(5)
topPoints = [(0,0,1)] + [(s*cos(i*2*pi/5.), s*sin(i*2*pi/5.), c) for i in range(5)]
bottomPoints = [(-x,y,-z) for (x,y,z) in topPoints]
icoPoints = topPoints + bottomPoints
icoTriangs = [(0,i+1,(i+1)%5+1) for i in range(5)] +\
[(6,i+7,(i+1)%5+7) for i in range(5)] +\
[(i+1,(i+1)%5+1,(7-i)%5+7) for i in range(5)] +\
[(i+1,(7-i)%5+7,(8-i)%5+7) for i in range(5)]
And here is the Python code to map (points of) the fixed triangle to a spherical triangle using a double slerp:
# barycentric coords for triangle (-0.5,0),(0.5,0),(0,sqrt(3)/2)
def barycentricCoords(p):
x,y = p
# l3*sqrt(3)/2 = y
l3 = y*2./sqrt(3.)
# l1 + l2 + l3 = 1
# 0.5*(l2 - l1) = x
l2 = x + 0.5*(1 - l3)
l1 = 1 - l2 - l3
return l1,l2,l3
from math import atan2
def scalProd(p1,p2):
return sum([p1[i]*p2[i] for i in range(len(p1))])
# uniform interpolation of arc defined by p0, p1 (around origin)
# t=0 -> p0, t=1 -> p1
def slerp(p0,p1,t):
assert abs(scalProd(p0,p0) - scalProd(p1,p1)) < 1e-7
ang0Cos = scalProd(p0,p1)/scalProd(p0,p0)
ang0Sin = sqrt(1 - ang0Cos*ang0Cos)
ang0 = atan2(ang0Sin,ang0Cos)
l0 = sin((1-t)*ang0)
l1 = sin(t *ang0)
return tuple([(l0*p0[i] + l1*p1[i])/ang0Sin for i in range(len(p0))])
# map 2D point p to spherical triangle s1,s2,s3 (3D vectors of equal length)
def mapGridpoint2Sphere(p,s1,s2,s3):
l1,l2,l3 = barycentricCoords(p)
if abs(l3-1) < 1e-10: return s3
l2s = l2/(l1+l2)
p12 = slerp(s1,s2,l2s)
return slerp(p12,s3,l3)
[Complete re-edit 18.10.2017]
the geometry storage is on you. Either you store it in some kind of Mesh or you generate it on the fly. I prefer to store it. In form of 2 tables. One holding all the vertexes (no duplicates) and the other holding 6 indexes of used points per each hex you got and some aditional info like spherical position to ease up the post processing.
Now how to generate this:
create hex triangle
the size should be radius of your sphere. do not include the corner hexess and also skip last line of the triangle (on both radial and axial so there is 1 hex gap between neighbor triangles on sphere) as that would overlap when joining out triangle segments.
convert 60deg hexagon triangle to 72deg pie
so simply convert to polar coordiantes (radius,angle), center triangle around 0 deg. Then multiply radius by cos(angle)/cos(30); which will convert triangle into Pie. And then rescale angle with ratio 72/60. That will make our triangle joinable...
copy&rotate triangle to fill 5 segments of pentagon
easy just rotate the points of first triangle and store as new one.
compute z
based on this Hexagonal tilling of hemi-sphere you can convert distance in 2D map into arc-length to limit the distortions as much a s possible.
However when I tried it (example below) the hexagons are a bit distorted so the depth and scaling needs some tweaking. Or post processing latter.
copy the half sphere to form a sphere
simply copy the points/hexes and negate z axis (or rotate by 180 deg if you want to preserve winding).
add equator and all of the missing pentagons and hexes
You should use the coordinates of the neighboring hexes so no more distortion and overlaps are added to the grid. Here preview:
Blue is starting triangle. Darker blue are its copies. Red are pole pentagons. Dark green is the equator, Lighter green are the join lines between triangles. In Yellowish are the missing equator hexagons near Dark Orange pentagons.
Here simple C++ OpenGL example (made from the linked answer in #4):
//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#pragma hdrstop
#include "win_main.h"
#include "gl/OpenGL3D_double.cpp"
#include "PolyLine.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main;
OpenGLscreen scr;
bool _redraw=true;
double animx= 0.0,danimx=0.0;
double animy= 0.0,danimy=0.0;
//---------------------------------------------------------------------------
PointTab pnt; // (x,y,z)
struct _hexagon
{
int ix[6]; // index of 6 points, last point duplicate for pentagon
int a,b; // spherical coordinate
DWORD col; // color
// inline
_hexagon() {}
_hexagon(_hexagon& a) { *this=a; }
~_hexagon() {}
_hexagon* operator = (const _hexagon *a) { *this=*a; return this; }
//_hexagon* operator = (const _hexagon &a) { ...copy... return this; }
};
List<_hexagon> hex;
//---------------------------------------------------------------------------
// https://stackoverflow.com/a/46787885/2521214
//---------------------------------------------------------------------------
void hex_sphere(int N,double R)
{
const double c=cos(60.0*deg);
const double s=sin(60.0*deg);
const double sy= R/(N+N-2);
const double sz=sy/s;
const double sx=sz*c;
const double sz2=0.5*sz;
const int na=5*(N-2);
const int nb= N;
const int b0= N;
double *q,p[3],ang,len,l,l0,ll;
int i,j,n,a,b,ix;
_hexagon h,*ph;
hex.allocate(na*nb);
hex.num=0;
pnt.reset3D(N*N);
b=0; a=0; ix=0;
// generate triangle hex grid
h.col=0x00804000;
for (b=1;b<N-1;b++) // skip first line b=0
for (a=1;a<b;a++) // skip first and last line
{
p[0]=double(a )*(sx+sz);
p[1]=double(b-(a>>1))*(sy*2.0);
p[2]=0.0;
if (int(a&1)!=0) p[1]-=sy;
ix=pnt.add(p[0]+sz2+sx,p[1] ,p[2]); h.ix[0]=ix; // 2 1
ix=pnt.add(p[0]+sz2 ,p[1]+sy,p[2]); h.ix[1]=ix; // 3 0
ix=pnt.add(p[0]-sz2 ,p[1]+sy,p[2]); h.ix[2]=ix; // 4 5
ix=pnt.add(p[0]-sz2-sx,p[1] ,p[2]); h.ix[3]=ix;
ix=pnt.add(p[0]-sz2 ,p[1]-sy,p[2]); h.ix[4]=ix;
ix=pnt.add(p[0]+sz2 ,p[1]-sy,p[2]); h.ix[5]=ix;
h.a=a;
h.b=N-1-b;
hex.add(h);
} n=hex.num; // remember number of hexs for the first triangle
// distort points to match area
for (ix=0;ix<pnt.nn;ix+=3)
{
// point pointer
q=pnt.pnt.dat+ix;
// convert to polar coordinates
ang=atan2(q[1],q[0]);
len=vector_len(q);
// match area of pentagon (72deg) triangle as we got hexagon (60deg) triangle
ang-=60.0*deg; // rotate so center of generated triangle is angle 0deg
while (ang>+60.0*deg) ang-=pi2;
while (ang<-60.0*deg) ang+=pi2;
len*=cos(ang)/cos(30.0*deg); // scale radius so triangle converts to pie
ang*=72.0/60.0; // scale up angle so rotated triangles merge
// convert back to cartesian
q[0]=len*cos(ang);
q[1]=len*sin(ang);
}
// copy and rotate the triangle to cover pentagon
h.col=0x00404000;
for (ang=72.0*deg,a=1;a<5;a++,ang+=72.0*deg)
for (ph=hex.dat,i=0;i<n;i++,ph++)
{
for (j=0;j<6;j++)
{
vector_copy(p,pnt.pnt.dat+ph->ix[j]);
rotate2d(-ang,p[0],p[1]);
h.ix[j]=pnt.add(p[0],p[1],p[2]);
}
h.a=ph->a+(a*(N-2));
h.b=ph->b;
hex.add(h);
}
// compute z
for (q=pnt.pnt.dat,ix=0;ix<pnt.nn;ix+=pnt.dn,q+=pnt.dn)
{
q[2]=0.0;
ang=vector_len(q)*0.5*pi/R;
q[2]=R*cos(ang);
ll=fabs(R*sin(ang)/sqrt((q[0]*q[0])+(q[1]*q[1])));
q[0]*=ll;
q[1]*=ll;
}
// copy and mirror the other half-sphere
n=hex.num;
for (ph=hex.dat,i=0;i<n;i++,ph++)
{
for (j=0;j<6;j++)
{
vector_copy(p,pnt.pnt.dat+ph->ix[j]);
p[2]=-p[2];
h.ix[j]=pnt.add(p[0],p[1],p[2]);
}
h.a= ph->a;
h.b=-ph->b;
hex.add(h);
}
// create index search table
int i0,i1,j0,j1,a0,a1,ii[5];
int **ab=new int*[na];
for (a=0;a<na;a++)
{
ab[a]=new int[nb+nb+1];
for (b=-nb;b<=nb;b++) ab[a][b0+b]=-1;
}
n=hex.num;
for (ph=hex.dat,i=0;i<n;i++,ph++) ab[ph->a][b0+ph->b]=i;
// add join ring
h.col=0x00408000;
for (a=0;a<na;a++)
{
h.a=a;
h.b=0;
a0=a;
a1=a+1; if (a1>=na) a1-=na;
i0=ab[a0][b0+1];
i1=ab[a1][b0+1];
j0=ab[a0][b0-1];
j1=ab[a1][b0-1];
if ((i0>=0)&&(i1>=0))
if ((j0>=0)&&(j1>=0))
{
h.ix[0]=hex[i1].ix[1];
h.ix[1]=hex[i0].ix[0];
h.ix[2]=hex[i0].ix[1];
h.ix[3]=hex[j0].ix[1];
h.ix[4]=hex[j0].ix[0];
h.ix[5]=hex[j1].ix[1];
hex.add(h);
ab[h.a][b0+h.b]=hex.num-1;
}
}
// add 2x5 join lines
h.col=0x00008040;
for (a=0;a<na;a+=N-2)
for (b=1;b<N-3;b++)
{
// +b hemisphere
h.a= a;
h.b=+b;
a0=a-b; if (a0< 0) a0+=na; i0=ab[a0][b0+b+0];
a0--; if (a0< 0) a0+=na; i1=ab[a0][b0+b+1];
a1=a+1; if (a1>=na) a1-=na; j0=ab[a1][b0+b+0];
j1=ab[a1][b0+b+1];
if ((i0>=0)&&(i1>=0))
if ((j0>=0)&&(j1>=0))
{
h.ix[0]=hex[i0].ix[5];
h.ix[1]=hex[i0].ix[4];
h.ix[2]=hex[i1].ix[5];
h.ix[3]=hex[j1].ix[3];
h.ix[4]=hex[j0].ix[4];
h.ix[5]=hex[j0].ix[3];
hex.add(h);
}
// -b hemisphere
h.a= a;
h.b=-b;
a0=a-b; if (a0< 0) a0+=na; i0=ab[a0][b0-b+0];
a0--; if (a0< 0) a0+=na; i1=ab[a0][b0-b-1];
a1=a+1; if (a1>=na) a1-=na; j0=ab[a1][b0-b+0];
j1=ab[a1][b0-b-1];
if ((i0>=0)&&(i1>=0))
if ((j0>=0)&&(j1>=0))
{
h.ix[0]=hex[i0].ix[5];
h.ix[1]=hex[i0].ix[4];
h.ix[2]=hex[i1].ix[5];
h.ix[3]=hex[j1].ix[3];
h.ix[4]=hex[j0].ix[4];
h.ix[5]=hex[j0].ix[3];
hex.add(h);
}
}
// add pentagons at poles
_hexagon h0,h1;
h0.col=0x00000080;
h0.a=0; h0.b=N-1; h1=h0; h1.b=-h1.b;
p[2]=sqrt((R*R)-(sz*sz));
for (ang=0.0,a=0;a<5;a++,ang+=72.0*deg)
{
p[0]=2.0*sz*cos(ang);
p[1]=2.0*sz*sin(ang);
h0.ix[a]=pnt.add(p[0],p[1],+p[2]);
h1.ix[a]=pnt.add(p[0],p[1],-p[2]);
}
h0.ix[5]=h0.ix[4]; hex.add(h0);
h1.ix[5]=h1.ix[4]; hex.add(h1);
// add 5 missing hexagons at poles
h.col=0x00600060;
for (ph=&h0,b=N-3,h.b=N-2,i=0;i<2;i++,b=-b,ph=&h1,h.b=-h.b)
{
a = 1; if (a>=na) a-=na; ii[0]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[1]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[2]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[3]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[4]=ab[a][b0+b];
for (j=0;j<5;j++)
{
h.a=((4+j)%5)*(N-2)+1;
h.ix[0]=ph->ix[ (5-j)%5 ];
h.ix[1]=ph->ix[ (6-j)%5 ];
h.ix[2]=hex[ii[(j+4)%5]].ix[4];
h.ix[3]=hex[ii[(j+4)%5]].ix[5];
h.ix[4]=hex[ii[ j ]].ix[3];
h.ix[5]=hex[ii[ j ]].ix[4];
hex.add(h);
}
}
// add 2*5 pentagons and 2*5 missing hexagons at equator
h0.a=0; h0.b=N-1; h1=h0; h1.b=-h1.b;
for (ang=36.0*deg,a=0;a<na;a+=N-2,ang-=72.0*deg)
{
p[0]=R*cos(ang);
p[1]=R*sin(ang);
p[2]=sz;
i0=pnt.add(p[0],p[1],+p[2]);
i1=pnt.add(p[0],p[1],-p[2]);
a0=a-1;if (a0< 0) a0+=na;
a1=a+1;if (a1>=na) a1-=na;
ii[0]=ab[a0][b0-1]; ii[2]=ab[a1][b0-1];
ii[1]=ab[a0][b0+1]; ii[3]=ab[a1][b0+1];
// hexagons
h.col=0x00008080;
h.a=a; h.b=0;
h.ix[0]=hex[ii[0]].ix[0];
h.ix[1]=hex[ii[0]].ix[1];
h.ix[2]=hex[ii[1]].ix[1];
h.ix[3]=hex[ii[1]].ix[0];
h.ix[4]=i0;
h.ix[5]=i1;
hex.add(h);
h.a=a; h.b=0;
h.ix[0]=hex[ii[2]].ix[2];
h.ix[1]=hex[ii[2]].ix[1];
h.ix[2]=hex[ii[3]].ix[1];
h.ix[3]=hex[ii[3]].ix[2];
h.ix[4]=i0;
h.ix[5]=i1;
hex.add(h);
// pentagons
h.col=0x000040A0;
h.a=a; h.b=0;
h.ix[0]=hex[ii[0]].ix[0];
h.ix[1]=hex[ii[0]].ix[5];
h.ix[2]=hex[ii[2]].ix[3];
h.ix[3]=hex[ii[2]].ix[2];
h.ix[4]=i1;
h.ix[5]=i1;
hex.add(h);
h.a=a; h.b=0;
h.ix[0]=hex[ii[1]].ix[0];
h.ix[1]=hex[ii[1]].ix[5];
h.ix[2]=hex[ii[3]].ix[3];
h.ix[3]=hex[ii[3]].ix[2];
h.ix[4]=i0;
h.ix[5]=i0;
hex.add(h);
}
// release index search table
for (a=0;a<na;a++) delete[] ab[a];
delete[] ab;
}
//---------------------------------------------------------------------------
void hex_draw(GLuint style) // draw hex
{
int i,j;
_hexagon *h;
for (h=hex.dat,i=0;i<hex.num;i++,h++)
{
if (style==GL_POLYGON) glColor4ubv((BYTE*)&h->col);
glBegin(style);
for (j=0;j<6;j++) glVertex3dv(pnt.pnt.dat+h->ix[j]);
glEnd();
}
if (0)
if (style==GL_POLYGON)
{
scr.text_init_pixel(0.1,-0.2);
glColor3f(1.0,1.0,1.0);
for (h=hex.dat,i=0;i<hex.num;i++,h++)
if (abs(h->b)<2)
{
double p[3];
vector_ld(p,0.0,0.0,0.0);
for (j=0;j<6;j++)
vector_add(p,p,pnt.pnt.dat+h->ix[j]);
vector_mul(p,p,1.0/6.0);
scr.text(p[0],p[1],p[2],AnsiString().sprintf("%i,%i",h->a,h->b));
}
scr.text_exit_pixel();
}
}
//---------------------------------------------------------------------------
void TMain::draw()
{
scr.cls();
int x,y;
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0,0.0,-5.0);
glRotated(animx,1.0,0.0,0.0);
glRotated(animy,0.0,1.0,0.0);
hex_draw(GL_POLYGON);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0,0.0,-5.0+0.01);
glRotated(animx,1.0,0.0,0.0);
glRotated(animy,0.0,1.0,0.0);
glColor3f(1.0,1.0,1.0);
glLineWidth(2);
hex_draw(GL_LINE_LOOP);
glCirclexy(0.0,0.0,0.0,1.5);
glLineWidth(1);
scr.exe();
scr.rfs();
}
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
{
scr.init(this);
hex_sphere(10,1.5);
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender)
{
scr.exit();
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormPaint(TObject *Sender)
{
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender)
{
scr.resize();
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60,float(scr.xs)/float(scr.ys),0.1,100.0);
_redraw=true;
}
//-----------------------------------------------------------------------
void __fastcall TMain::Timer1Timer(TObject *Sender)
{
animx+=danimx; if (animx>=360.0) animx-=360.0; _redraw=true;
animy+=danimy; if (animy>=360.0) animy-=360.0; _redraw=true;
if (_redraw) { draw(); _redraw=false; }
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift)
{
Caption=Key;
if (Key==40){ animx+=2.0; _redraw=true; }
if (Key==38){ animx-=2.0; _redraw=true; }
if (Key==39){ animy+=2.0; _redraw=true; }
if (Key==37){ animy-=2.0; _redraw=true; }
}
//---------------------------------------------------------------------------
I know it is a bit of a index mess and also winding rule is not guaranteed as I was too lazy to made uniform indexing. Beware the a indexes of each hex are not linear and if you want to use them to map to 2D map you would need to recompute it using atan2 on x,y of its center point position.
Here previews:
Still some distortions are present. They are caused by fact that we using 5 triangles to connect at equator (so connection is guaranteed). That means the circumference is 5*R instead of 6.28*R. How ever this can be still improved by a field simulation. Just take all the points and add retractive forces based on their distance and bound to sphere surface. Run simulation and when the oscillations lower below threshold you got your sphere grid ...
Another option would be find out some equation to remap the grid points (similarly what I done for triangle to pie conversion) that would have better results.
I'm currently implementing a line-of-sight algorithm that will tell me the points I can and cannot see along a line. So, if I am standing atop some hilly terrain, I can know where I can and cannot see.
The line that is generated from point A to point B includes a number of points evenly spaced between A and B. Starting from A, I see what the angle of elevation is between A and B. I note that as my alphaAngle.
Next, for each point between A and B, I get the angle of elevation between A and that point. If that point is the highest angle of elevation so far(excluding alphaAngle), then I mark it as such. Otherwise, it has a lower angle of elevation and therefor I should not be able to see it, and mark that point as having hasLOS = false.
Here are some object definitions I use:
struct TerrainProfilPnt
{
double m_x_lon; //lon
double m_y_lat; //lat
double m_z_elev; //elev
bool m_hasLOS; //Does this point have line of sight from another point?
};
class TerrainProfilePartitioner; // Holds a collection of points that make up the line
Here is my algorithm written out, however it doesn't return the correct results. Either it claims it has LOS when it shouldn't (like going from behind one hill to the opposite side of another hill, I shouldn't be able to see that).
Or it claims that I cannot see the end point when I should (Top of a hill to the valley beneath it). So, I am suspecting that either my implementation of finding the line-of-sight is incorrect, or I am implementing it wrong in code.
using Point = TerrainProfilePnt;
auto pointsInLine = terrainProfilePartitioner->GetPoints();
auto& pointsVec = pointsInLine->GetVector();
std::vector<Point> terrainProfileVec;
terrainProfileVec.reserve(pointsVec.size());
double start_xlon = 0.0f;
double start_ylat = 0.0f;
double start_zelev = 0.0f;
double end_xlon = 0.0f;
double end_ylat = 0.0f;
double end_zelev = 0.0f;
//The angle between the starting point and the ending point
double initialElevAngle = 0.0f;
//Assemble the first and last points
start_xlon = pointsVec.front().x();
start_ylat = pointsVec.front().y();
GetPointElevation(start_xlon, start_ylat, start_zelev);
end_xlon = pointsVec.back().x();
end_ylat = pointsVec.back().y();
GetPointElevation(end_xlon, end_ylat, end_zelev);
//Calculate the angle between the beginning and ending points
initialElevAngle = atan2(start_zelev, end_zelev) * 180 / M_PI;
Point initialPoint;
initialPoint.m_x_lon = start_xlon;
initialPoint.m_y_lat = start_ylat;
initialPoint.m_z_elev = start_zelev;
initialPoint.m_hasLOS = true;
//pointsVec.push_back(initialPoint);
terrainProfileVec.push_back(initialPoint);
double oldAngle = 0.0f;;
bool hasOldAngle = false;
for (std::size_t i = 1; i < pointsVec.size(); ++i)
{
Point p;
p.m_x_lon = pointsVec.at(i).x();
p.m_y_lat = pointsVec.at(i).y();
GetPointElevation(p.m_x_lon, p.m_y_lat, p.m_z_elev);
double newAngle = atan2(start_zelev, p.m_z_elev) * 180 / M_PI;
if (!hasOldAngle)
{
hasOldAngle = true;
oldAngle = newAngle;
p.m_hasLOS = true;
}
else
{
if (newAngle < oldAngle)
{
p.m_hasLOS = false;
}
else
{
oldAngle = newAngle;
p.m_hasLOS = true;
}
}
}
auto terrainPrfileSeq = new Common::ObjectRandomAccessSequence<TerrainProfilePnt>(terrainProfileVec);
return terrainPrfile
atan2(start_zelev, p.m_z_elev) is meaningless. You need
atan2(distance, p.m_z_elev - start_zelev);
where distance is horizontal distance between p and initialPoint.
I see few problems: You are not sorting the points by distance from A ascending and use only elevation. Also using goniometrics for each map location which is probably slow. I would use different approach instead:
set LOS=false for whole map
write a DDA line interpolation
P(t)=A+(B-A).t
where (B-A) is your viewing direction and t is parameter incrementing by step smaller then your grid size (so you do not miss anything). This would give you long,lat without any slow goniometrics.
Then find the object on your map. If it is topology sorted then this is either O(1) or O(log(n).log(m))
test each actual location P(t)
for visibility. If true set LOS=true and loop #2 otherwise stop the whole thing.
btw this is sometimes called ray-casting.
In the context of a game program, I have a moving circle and a fixed line segment. The segment can have an arbitrary size and orientation.
I know the radius of the circle: r
I know the coordinates of the circle before the move: (xC1, yC1)
I know the coordinates of the circle after the move: (xC2, yC2)
I know the coordinates of the extremities of the line segment: (xL1, yL1) - (xL2, yL2)
I am having difficulties trying to compute:
A boolean: If any part of the circle hits the line segment while moving from (xC1, yC1) to (xC2, yC2)
If the boolean is true, the coordinates (x, y) of the center of the circle when it hits the line segment (I mean when circle is tangent to segment for the first time)
I'm going to answer with pseudo-algorithm - without any code. The way I see it there are two cases in which we might return true, as per the image below:
Here in blue are your circles, the dashed line is the trajectory line and the red line is your given line.
We build a helper trajectory line, from and to the center of both circles. If this trajectory line intersects the given line - return true. See this question on how to compute that intersection.
In the second case the first test has failed us, but it might just so happen that the circles nudged the line as they passed on the trajectory anyway. We will need the following constuction:
From the trajectory we build normal lines to each point A and B. Then these lines are chopped or extended into helper lines (Ha and Hb), so that their length from A and B is exactly the radius of the circle. Then we check if each of these helper lines intersects with the trajectory line. If they do return true.
Otherwise return false.
Look here:
Line segment / Circle intersection
If the value you get under the square root of either the computation of x or y is negative, then the segment does not intersect. Aside from that, you can stop your computation after you have x and y (note: you may get two answers)
Update I've revised my answer to very specifically address your problem. I give credit to Doswa for this solution, as I pretty much followed along and wrote it for C#. The basic strategy is that we are going to locate the closest point of your line segment to the center of the circle. Based on that, we'll look at the distance of that closest point, and if it is within the radius, locate the point along the direction to the closest point that lies right at the radius of the circle.
// I'll bet you already have one of these.
public class Vec : Tuple<double, double>
{
public Vec(double item1, double item2) : base(item1, item2) { }
public double Dot(Vec other)
{ return Item1*other.Item1 + Item2*other.Item2; }
public static Vec operator-(Vec first, Vec second)
{ return new Vec(first.Item1 - second.Item1, first.Item2 - second.Item2);}
public static Vec operator+(Vec first, Vec second)
{ return new Vec(first.Item1 + second.Item1, first.Item2 + second.Item2);}
public static Vec operator*(double first, Vec second)
{ return new Vec(first * second.Item1, first * second.Item2);}
public double Length() { return Math.Sqrt(Dot(this)); }
public Vec Normalize() { return (1 / Length()) * this; }
}
public bool IntersectCircle(Vec origin, Vec lineStart,
Vec lineEnd, Vec circle, double radius, out Vec circleWhenHit)
{
circleWhenHit = null;
// find the closest point on the line segment to the center of the circle
var line = lineEnd - lineStart;
var lineLength = line.Length();
var lineNorm = (1/lineLength)*line;
var segmentToCircle = circle - lineStart;
var closestPointOnSegment = segmentToCircle.Dot(line) / lineLength;
// Special cases where the closest point happens to be the end points
Vec closest;
if (closestPointOnSegment < 0) closest = lineStart;
else if (closestPointOnSegment > lineLength) closest = lineEnd;
else closest = lineStart + closestPointOnSegment*lineNorm;
// Find that distance. If it is less than the radius, then we
// are within the circle
var distanceFromClosest = circle - closest;
var distanceFromClosestLength = distanceFromClosest.Length();
if (distanceFromClosestLength > radius) return false;
// So find the distance that places the intersection point right at
// the radius. This is the center of the circle at the time of collision
// and is different than the result from Doswa
var offset = (radius - distanceFromClosestLength) *
((1/distanceFromClosestLength)*distanceFromClosest);
circleWhenHit = circle - offset;
return true;
}
Here is some Java that calculates the distance from a point to a line (this is not complete, but will give you the basic picture). The code comes from a class called
'Vector'. The assumption is that the vector object is initialized to the line vector. The method 'distance' accepts the point that the line vector starts at (called 'at' of course), and the point of interest. It calculates and returns the distance from that point to the line.
public class Vector
{
double x_ = 0;
double y_ = 0;
double magnitude_ = 1;
public Vector()
{
}
public Vector(double x,double y)
{
x_ = x;
y_ = y;
}
public Vector(Vector other)
{
x_ = other.x_;
y_ = other.y_;
}
public void add(Vector other)
{
x_ += other.x_;
y_ += other.y_;
}
public void scale(double val)
{
x_ *= val;
y_ *= val;
}
public double dot(Vector other)
{
return x_*other.x_+y_*other.y_;
}
public void cross(Vector other)
{
x_ = x_*other.y_ - y_*other.x_;
}
public void unit()
{
magnitude_ = Math.sqrt(x_*x_+y_*y_);
x_/=magnitude_;
y_/=magnitude_;
}
public double distance(Vector at,Vector point)
{
//
// Create a perpendicular vector
//
Vector perp = new Vector();
perp.perpendicular(this);
perp.unit();
Vector offset = new Vector(point.x_ - at.x_,point.y_ - at.y_);
double d = Math.abs(offset.dot(perp));
double m = magnitude();
double t = dot(offset)/(m*m);
if(t < 0)
{
offset.x_ -= at.x_;
offset.y_ -= at.y_;
d = offset.magnitude();
}
if(t > 1)
{
offset.x_ -= at.x_+x_;
offset.y_ -= at.y_+y_;
d = offset.magnitude();
}
return d;
}
private void perpendicular(Vector other)
{
x_ = -other.y_;
y_ = other.x_;
}
public double magnitude()
{
magnitude_ = Math.sqrt(x_*x_+y_*y_);
return magnitude_;
}
}