Advanced vector math

Planes

The dot product has another interesting property with unit vectors. Imagine that perpendicular to that vector (and through the origin) passes a plane. Planes divide the entire space into positive (over the plane) and negative (under the plane), and (contrary to popular belief) you can also use their math in 2D:

../../_images/tutovec10.png

Unit vectors that are perpendicular to a surface (so, they describe the orientation of the surface) are called unit normal vectors. Though, usually they are just abbreviated as normals. Normals appear in planes, 3D geometry (to determine where each face or vertex is siding), etc. A normal is a unit vector, but it’s called normal because of its usage. (Just like we call (0,0) the Origin!).

The plane passes by the origin and the surface of it is perpendicular to the unit vector (or normal). The side the vector points to is the positive half-space, while the other side is the negative half-space. In 3D this is exactly the same, except that the plane is an infinite surface (imagine an infinite, flat sheet of paper that you can orient and is pinned to the origin) instead of a line.

Distance to plane

Now that it’s clear what a plane is, let’s go back to the dot product. The dot product between a unit vector and any point in space (yes, this time we do dot product between vector and position), returns the distance from the point to the plane:

GDScriptC#

  1. var distance = normal.dot(point)
  1. var distance = normal.Dot(point);

But not just the absolute distance, if the point is in the negative half space the distance will be negative, too:

../../_images/tutovec11.png

This allows us to tell which side of the plane a point is.

Away from the origin

I know what you are thinking! So far this is nice, but real planes are everywhere in space, not only passing through the origin. You want real plane action and you want it now.

Remember that planes not only split space in two, but they also have polarity. This means that it is possible to have perfectly overlapping planes, but their negative and positive half-spaces are swapped.

With this in mind, let’s describe a full plane as a normal N and a distance from the origin scalar D. Thus, our plane is represented by N and D. For example:

../../_images/tutovec12.png

For 3D math, Godot provides a Plane built-in type that handles this.

Basically, N and D can represent any plane in space, be it for 2D or 3D (depending on the amount of dimensions of N) and the math is the same for both. It’s the same as before, but D is the distance from the origin to the plane, travelling in N direction. As an example, imagine you want to reach a point in the plane, you will just do:

GDScriptC#

  1. var point_in_plane = N*D
  1. var pointInPlane = N * D;

This will stretch (resize) the normal vector and make it touch the plane. This math might seem confusing, but it’s actually much simpler than it seems. If we want to tell, again, the distance from the point to the plane, we do the same but adjusting for distance:

GDScriptC#

  1. var distance = N.dot(point) - D
  1. var distance = N.Dot(point) - D;

The same thing, using a built-in function:

GDScriptC#

  1. var distance = plane.distance_to(point)
  1. var distance = plane.DistanceTo(point);

This will, again, return either a positive or negative distance.

Flipping the polarity of the plane can be done by negating both N and D. This will result in a plane in the same position, but with inverted negative and positive half spaces:

GDScriptC#

  1. N = -N
  2. D = -D
  1. N = -N;
  2. D = -D;

Godot also implements this operator in Plane. So, using the format below will work as expected:

GDScriptC#

  1. var inverted_plane = -plane
  1. var invertedPlane = -plane;

So, remember, the plane’s main practical use is that we can calculate the distance to it. So, when is it useful to calculate the distance from a point to a plane? Let’s see some examples.

Constructing a plane in 2D

Planes clearly don’t come out of nowhere, so they must be built. Constructing them in 2D is easy, this can be done from either a normal (unit vector) and a point, or from two points in space.

In the case of a normal and a point, most of the work is done, as the normal is already computed, so calculate D from the dot product of the normal and the point.

GDScriptC#

  1. var N = normal
  2. var D = normal.dot(point)
  1. var N = normal;
  2. var D = normal.Dot(point);

For two points in space, there are actually two planes that pass through them, sharing the same space but with normal pointing to the opposite directions. To compute the normal from the two points, the direction vector must be obtained first, and then it needs to be rotated 90 degrees to either side:

GDScriptC#

  1. # Calculate vector from `a` to `b`.
  2. var dvec = point_a.direction_to(point_b)
  3. # Rotate 90 degrees.
  4. var normal = Vector2(dvec.y, -dvec.x)
  5. # Alternatively (depending the desired side of the normal):
  6. # var normal = Vector2(-dvec.y, dvec.x)
  1. // Calculate vector from `a` to `b`.
  2. var dvec = pointA.DirectionTo(pointB);
  3. // Rotate 90 degrees.
  4. var normal = new Vector2(dvec.Y, -dvec.X);
  5. // Alternatively (depending the desired side of the normal):
  6. // var normal = new Vector2(-dvec.Y, dvec.X);

The rest is the same as the previous example. Either point_a or point_b will work, as they are in the same plane:

GDScriptC#

  1. var N = normal
  2. var D = normal.dot(point_a)
  3. # this works the same
  4. # var D = normal.dot(point_b)
  1. var N = normal;
  2. var D = normal.Dot(pointA);
  3. // this works the same
  4. // var D = normal.Dot(pointB);

Doing the same in 3D is a little more complex and is explained further down.

Some examples of planes

Here is an example of what planes are useful for. Imagine you have a convex polygon. For example, a rectangle, a trapezoid, a triangle, or just any polygon where no faces bend inwards.

For every segment of the polygon, we compute the plane that passes by that segment. Once we have the list of planes, we can do neat things, for example checking if a point is inside the polygon.

We go through all planes, if we can find a plane where the distance to the point is positive, then the point is outside the polygon. If we can’t, then the point is inside.

../../_images/tutovec13.png

Code should be something like this:

GDScriptC#

  1. var inside = true
  2. for p in planes:
  3. # check if distance to plane is positive
  4. if (p.distance_to(point) > 0):
  5. inside = false
  6. break # with one that fails, it's enough
  1. var inside = true;
  2. foreach (var p in planes)
  3. {
  4. // check if distance to plane is positive
  5. if (p.DistanceTo(point) > 0)
  6. {
  7. inside = false;
  8. break; // with one that fails, it's enough
  9. }
  10. }

Pretty cool, huh? But this gets much better! With a little more effort, similar logic will let us know when two convex polygons are overlapping too. This is called the Separating Axis Theorem (or SAT) and most physics engines use this to detect collision.

With a point, just checking if a plane returns a positive distance is enough to tell if the point is outside. With another polygon, we must find a plane where all the other polygon points return a positive distance to it. This check is performed with the planes of A against the points of B, and then with the planes of B against the points of A:

../../_images/tutovec14.png

Code should be something like this:

GDScriptC#

  1. var overlapping = true
  2. for p in planes_of_A:
  3. var all_out = true
  4. for v in points_of_B:
  5. if (p.distance_to(v) < 0):
  6. all_out = false
  7. break
  8. if (all_out):
  9. # a separating plane was found
  10. # do not continue testing
  11. overlapping = false
  12. break
  13. if (overlapping):
  14. # only do this check if no separating plane
  15. # was found in planes of A
  16. for p in planes_of_B:
  17. var all_out = true
  18. for v in points_of_A:
  19. if (p.distance_to(v) < 0):
  20. all_out = false
  21. break
  22. if (all_out):
  23. overlapping = false
  24. break
  25. if (overlapping):
  26. print("Polygons Collided!")
  1. var overlapping = true;
  2. foreach (Plane plane in planesOfA)
  3. {
  4. var allOut = true;
  5. foreach (Vector3 point in pointsOfB)
  6. {
  7. if (plane.DistanceTo(point) < 0)
  8. {
  9. allOut = false;
  10. break;
  11. }
  12. }
  13. if (allOut)
  14. {
  15. // a separating plane was found
  16. // do not continue testing
  17. overlapping = false;
  18. break;
  19. }
  20. }
  21. if (overlapping)
  22. {
  23. // only do this check if no separating plane
  24. // was found in planes of A
  25. foreach (Plane plane in planesOfB)
  26. {
  27. var allOut = true;
  28. foreach (Vector3 point in pointsOfA)
  29. {
  30. if (plane.DistanceTo(point) < 0)
  31. {
  32. allOut = false;
  33. break;
  34. }
  35. }
  36. if (allOut)
  37. {
  38. overlapping = false;
  39. break;
  40. }
  41. }
  42. }
  43. if (overlapping)
  44. {
  45. GD.Print("Polygons Collided!");
  46. }

As you can see, planes are quite useful, and this is the tip of the iceberg. You might be wondering what happens with non convex polygons. This is usually just handled by splitting the concave polygon into smaller convex polygons, or using a technique such as BSP (which is not used much nowadays).

Collision detection in 3D

This is another bonus bit, a reward for being patient and keeping up with this long tutorial. Here is another piece of wisdom. This might not be something with a direct use case (Godot already does collision detection pretty well) but it’s used by almost all physics engines and collision detection libraries :)

Remember that converting a convex shape in 2D to an array of 2D planes was useful for collision detection? You could detect if a point was inside any convex shape, or if two 2D convex shapes were overlapping.

Well, this works in 3D too, if two 3D polyhedral shapes are colliding, you won’t be able to find a separating plane. If a separating plane is found, then the shapes are definitely not colliding.

To refresh a bit a separating plane means that all vertices of polygon A are in one side of the plane, and all vertices of polygon B are in the other side. This plane is always one of the face-planes of either polygon A or polygon B.

In 3D though, there is a problem to this approach, because it is possible that, in some cases a separating plane can’t be found. This is an example of such situation:

../../_images/tutovec22.png

To avoid it, some extra planes need to be tested as separators, these planes are the cross product between the edges of polygon A and the edges of polygon B

../../_images/tutovec23.png

So the final algorithm is something like:

GDScriptC#

  1. var overlapping = true
  2. for p in planes_of_A:
  3. var all_out = true
  4. for v in points_of_B:
  5. if (p.distance_to(v) < 0):
  6. all_out = false
  7. break
  8. if (all_out):
  9. # a separating plane was found
  10. # do not continue testing
  11. overlapping = false
  12. break
  13. if (overlapping):
  14. # only do this check if no separating plane
  15. # was found in planes of A
  16. for p in planes_of_B:
  17. var all_out = true
  18. for v in points_of_A:
  19. if (p.distance_to(v) < 0):
  20. all_out = false
  21. break
  22. if (all_out):
  23. overlapping = false
  24. break
  25. if (overlapping):
  26. for ea in edges_of_A:
  27. for eb in edges_of_B:
  28. var n = ea.cross(eb)
  29. if (n.length() == 0):
  30. continue
  31. var max_A = -1e20 # tiny number
  32. var min_A = 1e20 # huge number
  33. # we are using the dot product directly
  34. # so we can map a maximum and minimum range
  35. # for each polygon, then check if they
  36. # overlap.
  37. for v in points_of_A:
  38. var d = n.dot(v)
  39. max_A = max(max_A, d)
  40. min_A = min(min_A, d)
  41. var max_B = -1e20 # tiny number
  42. var min_B = 1e20 # huge number
  43. for v in points_of_B:
  44. var d = n.dot(v)
  45. max_B = max(max_B, d)
  46. min_B = min(min_B, d)
  47. if (min_A > max_B or min_B > max_A):
  48. # not overlapping!
  49. overlapping = false
  50. break
  51. if (not overlapping):
  52. break
  53. if (overlapping):
  54. print("Polygons collided!")
  1. var overlapping = true;
  2. foreach (Plane plane in planesOfA)
  3. {
  4. var allOut = true;
  5. foreach (Vector3 point in pointsOfB)
  6. {
  7. if (plane.DistanceTo(point) < 0)
  8. {
  9. allOut = false;
  10. break;
  11. }
  12. }
  13. if (allOut)
  14. {
  15. // a separating plane was found
  16. // do not continue testing
  17. overlapping = false;
  18. break;
  19. }
  20. }
  21. if (overlapping)
  22. {
  23. // only do this check if no separating plane
  24. // was found in planes of A
  25. foreach (Plane plane in planesOfB)
  26. {
  27. var allOut = true;
  28. foreach (Vector3 point in pointsOfA)
  29. {
  30. if (plane.DistanceTo(point) < 0)
  31. {
  32. allOut = false;
  33. break;
  34. }
  35. }
  36. if (allOut)
  37. {
  38. overlapping = false;
  39. break;
  40. }
  41. }
  42. }
  43. if (overlapping)
  44. {
  45. foreach (Vector3 edgeA in edgesOfA)
  46. {
  47. foreach (Vector3 edgeB in edgesOfB)
  48. {
  49. var normal = edgeA.Cross(edgeB);
  50. if (normal.Length() == 0)
  51. {
  52. continue;
  53. }
  54. var maxA = float.MinValue; // tiny number
  55. var minA = float.MaxValue; // huge number
  56. // we are using the dot product directly
  57. // so we can map a maximum and minimum range
  58. // for each polygon, then check if they
  59. // overlap.
  60. foreach (Vector3 point in pointsOfA)
  61. {
  62. var distance = normal.Dot(point);
  63. maxA = Mathf.Max(maxA, distance);
  64. minA = Mathf.Min(minA, distance);
  65. }
  66. var maxB = float.MinValue; // tiny number
  67. var minB = float.MaxValue; // huge number
  68. foreach (Vector3 point in pointsOfB)
  69. {
  70. var distance = normal.Dot(point);
  71. maxB = Mathf.Max(maxB, distance);
  72. minB = Mathf.Min(minB, distance);
  73. }
  74. if (minA > maxB || minB > maxA)
  75. {
  76. // not overlapping!
  77. overlapping = false;
  78. break;
  79. }
  80. }
  81. if (!overlapping)
  82. {
  83. break;
  84. }
  85. }
  86. }
  87. if (overlapping)
  88. {
  89. GD.Print("Polygons Collided!");
  90. }

More information

For more information on using vector math in Godot, see the following article:

If you would like additional explanation, you should check out 3Blue1Brown’s excellent video series “Essence of Linear Algebra”: https://www.youtube.com/watch?v=fNk_zzaMoSs&list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab


User-contributed notes

Please read the User-contributed notes policy before submitting a comment.

Previous Next