38

is there a easy formula to calculate this? i've been working on some math but i can only find a way to calculate the distance directed to the center of the box, not directed to the nearest point.. are there some resources on this problem?

Daniel
  • 391
  • 1
  • 4
  • 5
  • Is the box axis aligned? references to solutions for AABB and OBB are [here](http://www.realtimerendering.com/intersections.html) – bobobobo Jul 23 '13 at 00:44

9 Answers9

77

Here is a single formula that avoids all the case logic. (I happen to be working in JS right now, so here's a JS implementation). Let rect = {max:{x:_, y:_}, min:{x:_, y:_}} and p={x:_, y:_}

function distance(rect, p) {
  var dx = Math.max(rect.min.x - p.x, 0, p.x - rect.max.x);
  var dy = Math.max(rect.min.y - p.y, 0, p.y - rect.max.y);
  return Math.sqrt(dx*dx + dy*dy);
}

Explanation: This breaks down the problem into calculating the x distance dx and the y distance dy. It then uses distance formula.

For calculating dx, here is how that works. (dy is analogous)

Look at the tuple being provided to the max function: (min-p, 0, p-max). Let's designate this tuple (a,b,c).

If p is left of min, then we have p < min < max, which means the tuple will evaluate to (+,0,-), and so the max function will correctly return a = min - p.

If p is between min and max, then we have min < p < max, which means the tuple will evaluate to (-,0,-). So again, the max function will correctly return b = 0.

Lastly, if p is to the right of max, then we have, min < max < p, and the tuple evaluates to (-,0,+). Once again, Math.max correctly returns c = p - max.

So it turns out all the case logic is taken care of by Math.max, which leads to a nice 3-line, control-flow-free function.

MultiRRomero
  • 1,001
  • 8
  • 8
  • 2
    +1 This is a very nice, concrete solution. If the box is not oriented with the coordinate axes, it can still be used by rotating the problem. It doesn't really avoid "all the case logic"; it captures it (in each axis) with a single call to `Math.max`. I believe it even generalizes to 3D by simply adding an analogous `dz` component. – Ted Hopp Sep 07 '14 at 03:48
  • what if box has rotation? – M.kazem Akhgary Sep 07 '19 at 13:00
  • 1
    The last line could also be `return Math.hypot(dx, dy);`. This will usually be slower, but reduces the risk of underflow or overflow for extreme values of `dx` and/or `dy`. – Ted Hopp Dec 10 '19 at 21:25
9

I think you need to analyze cases; there's no single formula. It's easier to illustrate in two dimensions:

1          2          3
    +-------------+
    |             |
4   |      0      |   5
    |             |
    +-------------+
6          7          8

The edges of the box (extended) divide the outside into 9 regions. Region 0 (inside the box) is solved by computing the distance to each edge and taking the minimum. Every point in region 1 is closest to the top left vertex, and similarly for regions 3, 6, and 8. For regions 2, 4, 5, and 7, you need to find the distance from the point to the closest edge, which is a fairly simple problem. You can determine which region a point is in by classifying it with respect to each edge. (It's easier to see how to do this by directing the edges say, counter-clockwise.) This will also tell you if the point is inside the box.

In 3D, the logic is exactly the same except that you classify with respect to the six faces and you have more cases.

The problem is simpler if the edges of the box are parallel to the coordinate axes.

Ted Hopp
  • 222,293
  • 47
  • 371
  • 489
5

Let's say that the point is named P and ABCD is our rectangle. Then the problem can be decomposed into the following set of subproblems:

(1) Develop a function dist(P, AB) that calculates the distance between a point P and an arbitrary segment AB.

(2) Calculate four distances between your point P and each side of the rectangle (each side is a segment) and take the shortest of the four

  distance = min(dist(P, AB), dist(P,BC), dist(P, CD), dist(P, DA))

That's your answer.

Now, we need to know how to calculate the distance between point P and an arbitrary segment AB, i.e. how to calculate dist(P, AB). This is done as follows

(1) Perform a perpendicular projection of the point P to the line AB. You get the new point P' on AB.

(2) If P' lies between A and B, then dist(P, AB) is the distance between P and P'.

(3) Otherwise, dist(P, AB) is the distance between P and either A or B, whichever is shorter.

That's it. There are some obvious ways to optimize the procedure, but even if implemented literally, it will work very well already.

P.S. Of course, one can ask how to perform a projection of a point to a line. I'll leave it as an exercise to the reader :)

AnT
  • 291,388
  • 39
  • 487
  • 734
2

Kikito's answer is not correct, in fact if P is in the regions 2, 4, 5 or 7 of Ted Hopp's scheme, it returns the minimum distance from the vertices, which is different (bigger) from the minimum distance from the edges.

I would fix kikito's function distance_aux by returning 0 instead of min(p - lower, upper - p), and everything works apart from the 0 region where P is inside the box. In my opinion that region should be managed separately, depending on what you want to achieve, whether the distance from the area or the distance from the perimeter of the box. If you want to obtain the distance from the area of the box, I would say that it is zero when the point is inside the box.

function inside(point, box)
    return (point.x > box.left AND point.x < box.right AND point.y > box.top AND point.y < box.bottom)
end

function distance_aux(p, lower, upper)
    if p < lower then return lower - p end
    if p > upper then return p - upper end
    return 0
end

function distance(point, box)
    local dx = distance_aux(point.x, box.left, box.right)
    local dy = distance_aux(point.y, box.top, box.bottom)
    if (inside(point, box))
        return min(dx, dy)    // or 0 in case of distance from the area
    else
        return sqrt(dx * dx + dy * dy)
    endif
end
Gianluca
  • 133
  • 1
  • 8
1

Lightly optimized C# alternative (although there should probably be some tolerance when comparing doubles against 0). I also recommend creating some kind of Rect or Point extension methods for these.

public static class GeometryUtils
{
    public static double Distance(Point point, Rect rect)
    {
        var xDist = MinXDistance(point, rect);
        var yDist = MinYDistance(point, rect);
        if (xDist == 0)
        {
            return yDist;
        }
        else if (yDist == 0)
        {
            return xDist;
        }

        return Math.Sqrt(Math.Pow(xDist, 2) + Math.Pow(yDist, 2));
    }

    private static double MinXDistance(Point point, Rect rect)
    {
        if (rect.Left > point.X)
        {
            return rect.Left - point.X;
        }
        else if (rect.Right < point.X)
        {
            return point.X - rect.Right;
        }
        else
        {
            return 0;
        }
    }

    private static double MinYDistance(Point point, Rect rect)
    {
        if (rect.Bottom < point.Y)
        {
            return point.Y - rect.Bottom;
        }
        else if (rect.Top > point.Y)
        {
            return rect.Top - point.Y;
        }
        else
        {
            return 0;
        }
    }
}
Mo0gles
  • 9,447
  • 1
  • 19
  • 15
1

For AABBs:

Maybe not the best performing, but certainly the simplest method:

p = your point

c = centre of the cube

s = half size of the cube

r = the point we are looking for

v = p - c;
m = max_abs(v);
r = c + ( v / m * s );
Addi
  • 691
  • 1
  • 8
  • 26
  • If you keep the center of the cube constant and __p__ constant but rotate the cube then the closest point on the cube should move around. However your formula doesn't take into account cube orientation so it is clearly wrong. – bradgonesurfing Jun 29 '17 at 11:35
  • 1
    If you do a world to local transform beforehand this works for non-AABBs as well + you can skip the "c" part alltogether. – spaceemotion Jan 06 '19 at 14:44
1

This is easy to achieve with dot products. In fact it has been answered in 3d already for the non axis aligned case.

https://stackoverflow.com/a/44824522/158285

But in 2D you can achieve the same

public struct Vector2 {
   double X; double Y

   // Vector dot product
   double Dot(Vector2 other)=>X*other.X+Y*other.Y;


   // Length squared of the vector
   double LengthSquared()=>Dot(this,this);

   // Plus other methods for multiplying by a scalar
   // adding and subtracting vectors etc
}

The function to return the closest point

public Vector2 ClosestPointTo
    (Vector2 q, Vector2 origin, Vector3 v10, Vector3 v01)
{
    var px = v10;
    var py = v01;


    var vx = (px - origin);
    var vy = (py - origin);


    var tx = Vector2.Dot( q - origin, vx ) / vx.LengthSquared();
    var ty = Vector3.Dot( q - origin, vy ) / vy.LengthSquared();


    tx = tx < 0 ? 0 : tx > 1 ? 1 : tx;
    ty = ty < 0 ? 0 : ty > 1 ? 1 : ty;

    var p = tx * vx + ty * vy + origin;

    return p;
}
bradgonesurfing
  • 28,325
  • 12
  • 101
  • 188
-1

I have been looking for this and I think I have a solution, for the case on which the box is axis-aligned (a fairly common case)

I believe that in that case you can calculate the distance like this:

function distance_aux(p, lower, upper)
  if p < lower then return lower - p end
  if p > upper then return p - upper end
  return min(p - lower, upper - p)
end

function distance(point, box)
  local dx = distance_aux(point.x, box.left, box.right)
  local dy = distance_aux(point.y, box.top, box.bottom)
  return sqrt(dx * dx + dy * dy)
end

This can be extended to z, of course.

kikito
  • 48,656
  • 29
  • 134
  • 183
-1

Is this a 3D box or a 2D rectangle? Either way you're probably best off getting the point-line (for 2D) or point-plane (3D) distance for each side, then selecting the minimum.

Edit: there's a much better way described here (last post). It involves transforming your point coordinates into box space, then "saturating" the coordinates with the box size to find the point on the box closest to the point. I haven't tried it, but it looks right to me.

PunKeel
  • 767
  • 6
  • 11
Rob Agar
  • 11,794
  • 4
  • 41
  • 58