Point-to-plane distance and projection
With a unit normal, one dot product gives a point’s signed distance from a plane and its closest point on it. Learn when to project along the normal and when to drop vertically, and how to mirror in a plane.
Signed distance
s > 0: P is on the side the normal points to (above a floor, outside a wall face). s < 0: P is on the other side (below the floor, inside the wall). s = 0: P is on the plane. In practice, |s| < a tolerance.
var n = new V3(0, -0.6, 0.8); // unit normal of the roof from the last lessondouble d = -2.4; foreach (var p in new V3[] { new(4, 1, 5), new(4, 2, 3) }){ double s = V3.Dot(n, p) + d; // signed distance V3 q = p - n * s; // foot of the perpendicular Console.WriteLine($"{p}: s = {s:0.###}, {(s > 0 ? "above" : "below")} the roof, closest point {q}");} readonly record struct V3(double X, double Y, double Z){ public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k); public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; public static V3 Cross(V3 a, V3 b) => new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); public double Length => Math.Sqrt(Dot(this, this)); public V3 Normalized() => this * (1 / Length); public override string ToString() => $"({X:0.###}, {Y:0.###}, {Z:0.###})";}(4, 1, 5): s = 1, above the roof, closest point (4, 1.6, 4.2) (4, 2, 3): s = -1.2, below the roof, closest point (4, 1.28, 3.96)
Dropping vertically instead
var n = new V3(0, -0.6, 0.8);double d = -2.4;if (Math.Abs(n.Z) < 1e-9) throw new InvalidOperationException("A vertical plane has no height at a plan point"); foreach (var (x, y) in new[] { (4.0, 1.0), (4.0, 3.0), (7.5, 0.0) }){ double z = -(n.X * x + n.Y * y + d) / n.Z; // solve ax + by + cz + d = 0 for z Console.WriteLine($"Roof above ({x}, {y}) is at z = {z:F3}");} readonly record struct V3(double X, double Y, double Z){ public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k); public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; public static V3 Cross(V3 a, V3 b) => new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); public double Length => Math.Sqrt(Dot(this, this)); public V3 Normalized() => this * (1 / Length); public override string ToString() => $"({X:0.###}, {Y:0.###}, {Z:0.###})";}Roof above (4, 1) is at z = 3.750 Roof above (4, 3) is at z = 5.250 Roof above (7.5, 0) is at z = 3.000
Projecting a direction onto a plane
var n = new V3(0, -0.6, 0.8); // the roof's unit normalvar down = new V3(0, 0, -1); V3 along = down - n * V3.Dot(down, n); // remove the part along the normalV3 u = along.Normalized();Console.WriteLine($"Water runs along {u}");Console.WriteLine($"Slope of that line: {Math.Asin(-u.Z) * 180 / Math.PI:F2} deg"); readonly record struct V3(double X, double Y, double Z){ public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k); public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; public static V3 Cross(V3 a, V3 b) => new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); public double Length => Math.Sqrt(Dot(this, this)); public V3 Normalized() => this * (1 / Length); public override string ToString() => $"({X:0.###}, {Y:0.###}, {Z:0.###})";}Water runs along (0, -0.8, -0.6) Slope of that line: 36.87 deg
Mirror(p, n, d) and mirror a light fitting at (2, 3, 2.5) in the wall plane x = 5, and the point (4, 1, 5) in the roof plane from this lesson.
Show a hint
Mirror; it is negative for the light because the light is behind the wall’s normal.
Show the solution
var light = new V3(2, 3, 2.5);Console.WriteLine($"In the wall x = 5: {Mirror(light, new V3(1, 0, 0), -5)}");var dish = new V3(4, 1, 5);Console.WriteLine($"In the roof: {Mirror(dish, new V3(0, -0.6, 0.8), -2.4)}"); static V3 Mirror(V3 p, V3 n, double d) => p - n * (2 * (V3.Dot(n, p) + d)); readonly record struct V3(double X, double Y, double Z){ public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k); public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; public static V3 Cross(V3 a, V3 b) => new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); public double Length => Math.Sqrt(Dot(this, this)); public V3 Normalized() => this * (1 / Length); public override string ToString() => $"({X:0.###}, {Y:0.###}, {Z:0.###})";}In the wall x = 5: (8, 3, 2.5) In the roof: (4, 2.2, 3.4)
Remember
With a unit normal, s = n·P + d is the signed distance: positive in front, negative behind. The closest point of the plane is Q = P − s·n; there is nothing to clamp. To set something down on a roof or terrain, drop vertically: z = −(ax + by + d) ÷ c, if c ≠ 0. v − (v·n)·n projects a direction into the plane; projecting “down” gives the fall line. Mirroring in a plane is P − 2s·n.