Point in a box and a sphere, line by line
Step 1 — point in an AABB: between min and max on every axis. A point
\vec{p} is inside the axis-aligned box
[\vec{m}, \vec{M}] exactly when each of its coordinates falls in
that axis's interval — and it must hold on all axes at once:
\vec{m} \le \vec{p} \le \vec{M} \iff m_x \le p_x \le M_x \ \text{and}\ m_y \le p_y \le M_y \ \text{and}\ m_z \le p_z \le M_z.
Six comparisons, no arithmetic. The instant a coordinate falls outside its interval, the point
is out and you stop — the same early-out as the box overlap test.
Step 2 — point in a sphere: compare the squared distance. A point is inside
the sphere (\vec{c}, r) when it is no farther from the centre than
the radius. Test the squared
distance
so no square root is needed:
\lVert \vec{p} - \vec{c}\rVert^2 \le r^2 \iff (p_x - c_x)^2 + (p_y - c_y)^2 + (p_z - c_z)^2 \le r^2.
One subtraction, three squares, a sum, one comparison against a precomputed
r^2. This is the cheapest trigger-zone in any game.
Point in a triangle, line by line
Boxes and spheres are easy because they are defined by inequalities already. A triangle takes
one more idea — the same one a ray tracer uses to hit a mesh face.
Step 3 — write the point in barycentric coordinates. Given a triangle
A, B, C, any point in its plane is a weighted blend of the corners
with weights summing to one — its
barycentric coordinates
(u, v, w):
\vec{p} = u\,A + v\,B + w\,C, \qquad u + v + w = 1.
Step 4 — inside means all three weights are non-negative. Each weight is "how
much of that corner" is in the mix. A negative weight means the point has stepped past the edge
opposite that corner, out of the triangle. So:
\vec{p} \text{ is inside } \triangle ABC \iff u \ge 0,\ v \ge 0,\ w \ge 0.
Step 5 — the equivalent same-side test, with cross products. There is a
dual view that needs no division. The point is inside iff it lies on the same side of
all three directed edges. Walking the triangle A \to B \to C, the
2-D cross product (the signed area, z-component) of an edge with the
vector from the edge's start to \vec{p} must have a consistent sign:
(B - A)\times(\vec{p} - A), \quad (C - B)\times(\vec{p} - B), \quad (A - C)\times(\vec{p} - C) \quad\text{all share one sign.}
Each sign says "\vec{p} is to the left (or right) of this edge"; all
the same sign means the point is boxed in by all three edges — inside. These cross products are,
up to a scale by the triangle's area, exactly the barycentric weights of Step 4, so the two
tests agree. (And it is the same separating-axis instinct again: a single edge with the wrong
sign separates the point from the triangle.)
A point \vec{p} is inside:
-
an AABB [\vec{m}, \vec{M}] iff
m_a \le p_a \le M_a on every axis
a.
-
a sphere (\vec{c}, r) iff
\lVert \vec{p} - \vec{c}\rVert^2 \le r^2 (squared — no root).
-
a triangle ABC iff its barycentric weights are
all non-negative, u, v, w \ge 0 — equivalently,
\vec{p} is on the same side of all three edges (cross-product
signs agree).
Every click in a user interface is a point-in-shape test. A rectangular button is an AABB in
screen space: the cursor (p_x, p_y) hits it iff
m_x \le p_x \le M_x and
m_y \le p_y \le M_y — the six-comparison test of Step 1, run on
every mouse move to decide what to highlight. A round button or a radar blip is the squared
sphere test instead.
In the world, designers paint invisible trigger volumes — a box around a
doorway, a sphere around a campfire, a polygon outlining a capture zone — and the engine asks,
each frame, "is the player's position inside?". A box trigger is Step 1, a proximity trigger
is Step 2, and an irregular region is tessellated into triangles tested by Step 4. The same
three inequalities decide whether a cutscene fires, whether you take fire damage, and whether
the level even loads the next room. Hit-testing isn't a UI nicety — it is the same point math
everywhere.
Boxes and spheres have a soft blind spot on their boundary. A point sitting exactly
on the surface — p_x = M_x, or
\lVert \vec{p}-\vec{c}\rVert^2 = r^2 to the last bit — is
genuinely ambiguous: whether it counts as "inside" depends only on whether you wrote
\le or < in the test. Neither choice is
"more correct"; pick a convention (usually inclusive, so touching counts as inside) and apply
it consistently, especially where two shapes share an edge — you want exactly one of them to
claim a point sitting right on the seam, not both or neither.
Polygons hide a sharper trap. The standard trick for an irregular polygon shoots a ray from
\vec{p} out to infinity and counts how many edges it crosses — odd
means inside, even means outside. That counting breaks the moment the ray happens to pass
exactly through a vertex instead of cleanly through the middle of an edge: is that one
crossing, or does it belong to both edges that meet there, making it two? Get the bookkeeping
wrong and a perfectly ordinary point flips from inside to outside. The usual fix is a
consistent tie-breaking rule — treat a vertex as belonging to only one of its two edges (say,
count an edge only when one endpoint's y is
\ge the ray and the other's is strictly less) — so every vertex the
ray touches is resolved the same way, every time.