A floor, a wall, a ramp: in space, the flat surface that goes on for ever is the plane, and it plays the part the line played in Part I. It even has the same kind of equation. This chapter uses Vec3, subtract and dot from the chapter on coordinates in space, and the Sphere and Box types from the two chapters before it.
The equation of a plane
A line in the plane was . A plane in space takes one more term:
The vector is the plane’s normal vector. It points straight out of the plane, at right angles to every direction within it, just like did for a line. If you know one point of the plane and its normal, follows, because has to satisfy the equation:
The floor in these figures, for example, goes through the origin with normal : , or simply .
type Plane = { a: number; b: number; c: number; d: number };
// The plane through `point`, at right angles to `normal`.
function planeAt(point: Vec3, normal: Vec3): Plane {
return { a: normal.x, b: normal.y, c: normal.z, d: -dot(normal, point) };
}// A plane is an object like { a: 0, b: 1, c: 0, d: 0 }, for y = 0.
// The plane through `point`, at right angles to `normal`.
function planeAt(point, normal) {
return { a: normal.x, b: normal.y, c: normal.z, d: -dot(normal, point) };
}@dataclass
class Plane:
a: float
b: float
c: float
d: float
def plane_at(point: Vec3, normal: Vec3) -> Plane:
"""The plane through `point`, at right angles to `normal`."""
return Plane(normal.x, normal.y, normal.z, -dot(normal, point))planeAt- time O(1) space O(1)
The cross product
Often you don’t know the normal, only three points of the plane, like the corners of a triangle in a 3D model. Then you need a vector at right angles to two directions within the plane, and there’s an operation that makes exactly that, the cross product:
Unlike the dot product, it gives a vector, not a number, and it only exists in 3D.
- It’s perpendicular to both and . Dot it with either one and you get .
- Its length is , the area of the parallelogram the two vectors span, so it’s the zero vector when and are parallel.
- Order matters: points the opposite way. With the axes in this book, curl the fingers of your right hand from towards , and your thumb points along . With Unity’s axes, it’s your left hand.
function cross(u: Vec3, v: Vec3): Vec3 {
return {
x: u.y * v.z - u.z * v.y,
y: u.z * v.x - u.x * v.z,
z: u.x * v.y - u.y * v.x
};
}function cross(u, v) {
return {
x: u.y * v.z - u.z * v.y,
y: u.z * v.x - u.x * v.z,
z: u.x * v.y - u.y * v.x
};
}def cross(u: Vec3, v: Vec3) -> Vec3:
return Vec3(
u.y * v.z - u.z * v.y,
u.z * v.x - u.x * v.z,
u.x * v.y - u.y * v.x,
)cross- time O(1) space O(1)
A plane through three points
With three points , and , the vectors and both lie in the plane, so their cross product is its normal, and is a point on it:
Drag the points below and watch stay at right angles to both arrows. Swap and around and flips to the other side. Then line all three points up: the arrows become parallel, shrinks to nothing, and there’s no plane left, because three points on one line don’t pick out a single plane.
function planeThrough(a: Vec3, b: Vec3, c: Vec3): Plane {
return planeAt(a, cross(subtract(b, a), subtract(c, a)));
}function planeThrough(a, b, c) {
return planeAt(a, cross(subtract(b, a), subtract(c, a)));
}def plane_through(a: Vec3, b: Vec3, c: Vec3) -> Plane:
return plane_at(a, cross(subtract(b, a), subtract(c, a)))planeThrough- time O(1) space O(1)
Which side of a plane a point is on
It works just like it did for a line. Put a point into the left-hand side of the equation, and you get on the plane, a positive number on the side points to, and a negative number on the other side. In the figure above, turns green on the side points to and red on the other.
// ax + by + cz + d for the point: 0 on the plane, and its sign says which side.
function side(p: Vec3, plane: Plane): number {
return plane.a * p.x + plane.b * p.y + plane.c * p.z + plane.d;
}// ax + by + cz + d for the point: 0 on the plane, and its sign says which side.
function side(p, plane) {
return plane.a * p.x + plane.b * p.y + plane.c * p.z + plane.d;
}def side(p: Vec3, plane: Plane) -> float:
"""ax + by + cz + d for the point: 0 on the plane, and its sign says which side."""
return plane.a * p.x + plane.b * p.y + plane.c * p.z + plane.dside- time O(1) space O(1)
Distance from a point to a plane
As with a line, the side value grows with the distance from the plane, times the length of the normal vector. Divide that out, and you have the distance:
In the figure above, it’s the length of the dashed line from , which meets the plane at a right angle.
function distanceToPlane(p: Vec3, plane: Plane): number {
return Math.abs(side(p, plane)) / Math.hypot(plane.a, plane.b, plane.c);
}function distanceToPlane(p, plane) {
return Math.abs(side(p, plane)) / Math.hypot(plane.a, plane.b, plane.c);
}def distance_to_plane(p: Vec3, plane: Plane) -> float:
return abs(side(p, plane)) / math.hypot(plane.a, plane.b, plane.c)distanceToPlane- time O(1) space O(1)
If you keep the normal at length , the side value is the distance, with a sign, and there’s nothing to divide.
Sphere vs plane
A sphere touches a plane when its centre is at most away from it. Square both sides to skip the root and the division, just like circle vs line:
function spherePlaneCollide(s: Sphere, plane: Plane): boolean {
return side(s, plane) ** 2 <= s.r ** 2 * (plane.a ** 2 + plane.b ** 2 + plane.c ** 2);
}function spherePlaneCollide(s, plane) {
return side(s, plane) ** 2 <= s.r ** 2 * (plane.a ** 2 + plane.b ** 2 + plane.c ** 2);
}def sphere_plane_collide(s: Sphere, plane: Plane) -> bool:
return side(s, plane) ** 2 <= s.r ** 2 * (plane.a ** 2 + plane.b ** 2 + plane.c ** 2)spherePlaneCollide- time O(1) space O(1)
For the floor, , it boils down to . A floor or a wall usually has only one side that matters, though: a ball that ends up below the floor has gone through it, and that should count as a hit too. For that, drop the square and the absolute value, and test , and everything behind the plane counts as a hit.
Box vs plane
Project the box onto the normal, the way Part I projected polygons onto an axis for the separating axis test. The centre of the box lands at , and the corners spread out around it by at most
Each half-size of the box counts as much as the normal leans along that axis, and the absolute values make every one of them count outwards. The box touches the plane when the plane is within its reach:
In the figure, the bar through the centre is the box’s reach along , and the white dot is the corner that reaches the plane first. The box touches the plane just as the bar does.
function boxPlaneCollide(box: Box, plane: Plane): boolean {
const centre = { x: box.x + box.w / 2, y: box.y + box.h / 2, z: box.z + box.d / 2 };
const reach =
(box.w / 2) * Math.abs(plane.a) +
(box.h / 2) * Math.abs(plane.b) +
(box.d / 2) * Math.abs(plane.c);
return Math.abs(side(centre, plane)) <= reach;
}function boxPlaneCollide(box, plane) {
const centre = { x: box.x + box.w / 2, y: box.y + box.h / 2, z: box.z + box.d / 2 };
const reach =
(box.w / 2) * Math.abs(plane.a) +
(box.h / 2) * Math.abs(plane.b) +
(box.d / 2) * Math.abs(plane.c);
return Math.abs(side(centre, plane)) <= reach;
}def box_plane_collide(box: Box, plane: Plane) -> bool:
centre = Vec3(box.x + box.w / 2, box.y + box.h / 2, box.z + box.d / 2)
reach = (
box.w / 2 * abs(plane.a)
+ box.h / 2 * abs(plane.b)
+ box.d / 2 * abs(plane.c)
)
return abs(side(centre, plane)) <= reachboxPlaneCollide- time O(1) space O(1)
This is the separating axis test with a single axis, the normal: a plane is flat, so its own shadow on the normal is a single point, , and the box either covers it or it doesn’t.
Comments
No comments yet. Questions and corrections are welcome.