Ray–triangle intersection
Find where a ray meets a triangle with the Möller–Trumbore method, read the answer as barycentric weights, and use it for picking, terrain heights and inside/outside tests.
Two questions in one
Barycentric coordinates
Möller–Trumbore
V3 a = new(0, -2, 0), b = new(0, 2, 0.3), c = new(0, -0.4, 2.8); // a panel in the plane x = 0var o = new V3(4.5, -1.2, 2.4); var rays = new (string Name, V3 O, V3 D)[]{ ("aimed at (0, 0.2, 1)", o, new V3(0, 0.2, 1) - o), ("aimed at (0, 1.6, 2)", o, new V3(0, 1.6, 2) - o), ("pointing away", o, o - new V3(0, 0.2, 1)), ("parallel to the panel", new V3(1.2, -3.2, 1.2), new V3(0, 1, 0.15))};foreach (var (name, ro, d) in rays) Console.WriteLine(Intersect(ro, d, a, b, c) is double t ? $"{name}: hit at t = {t:0.###}, point {ro + d * t}" : $"{name}: miss"); static double? Intersect(V3 o, V3 d, V3 a, V3 b, V3 c){ const double Eps = 1e-12; V3 e1 = b - a, e2 = c - a; V3 p = V3.Cross(d, e2); double det = V3.Dot(e1, p); if (Math.Abs(det) < Eps) return null; // parallel to the plane double inv = 1 / det; V3 s = o - a; double u = V3.Dot(s, p) * inv; // weight of B if (u < 0 || u > 1) return null; V3 q = V3.Cross(s, e1); double v = V3.Dot(d, q) * inv; // weight of C if (v < 0 || u + v > 1) return null; double t = V3.Dot(e2, q) * inv; // distance along the ray, in units of d return t > Eps ? t : null; // only in front of the origin} 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); static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###"); public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}aimed at (0, 0.2, 1): hit at t = 1, point (0, 0.2, 1) aimed at (0, 1.6, 2): miss pointing away: miss parallel to the panel: miss
The nearest hit, and weights as an interpolator
// Two triangles of a surveyed terrain surfacevar pts = new V3[] { new(0, 0, 10.2), new(20, 0, 11.0), new(20, 20, 12.6), new(0, 20, 10.8) };var tris = new[] { (0, 1, 2), (0, 2, 3) }; foreach (var (x, y) in new[] { (5.0, 2.0), (12.0, 15.0), (25.0, 5.0) }){ var o = new V3(x, y, 1000); // drop a ray straight down var d = new V3(0, 0, -1); double? best = null; foreach (var (i, j, k) in tris) if (Intersect(o, d, pts[i], pts[j], pts[k]) is double t && (best is null || t < best)) best = t; Console.WriteLine(best is double tb ? $"({x}, {y}): ground at z = {1000 - tb:0.###}" : $"({x}, {y}): outside the survey");} static double? Intersect(V3 o, V3 d, V3 a, V3 b, V3 c){ V3 e1 = b - a, e2 = c - a, p = V3.Cross(d, e2); double det = V3.Dot(e1, p); if (Math.Abs(det) < 1e-12) return null; double inv = 1 / det; V3 s = o - a; double u = V3.Dot(s, p) * inv; if (u < 0 || u > 1) return null; V3 q = V3.Cross(s, e1); double v = V3.Dot(d, q) * inv; if (v < 0 || u + v > 1) return null; double t = V3.Dot(e2, q) * inv; return t > 1e-12 ? t : null;} 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); static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###"); public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}(5, 2): ground at z = 10.56 (12, 15): ground at z = 11.73 (25, 5): outside the survey
Speed: not every triangle
Show a hint
Show the solution
var box = Mesh.Box(4, 3, 2.5);var dir = new V3(1, 0.123, 0.0371); foreach (var p in new V3[] { new(2, 1.5, 1), new(5, 1, 1), new(1, 1, 2.4), new(2, 1.5, -0.5) }){ int hits = box.F.Count(t => Intersect(p, dir, box.V[t.A], box.V[t.B], box.V[t.C]) is not null); Console.WriteLine($"{p}: {hits} crossing{(hits == 1 ? "" : "s")} -> {(hits % 2 == 1 ? "inside" : "outside")}");} static double? Intersect(V3 o, V3 d, V3 a, V3 b, V3 c){ V3 e1 = b - a, e2 = c - a, p = V3.Cross(d, e2); double det = V3.Dot(e1, p); if (Math.Abs(det) < 1e-12) return null; double inv = 1 / det; V3 s = o - a; double u = V3.Dot(s, p) * inv; if (u < 0 || u > 1) return null; V3 q = V3.Cross(s, e1); double v = V3.Dot(d, q) * inv; if (v < 0 || u + v > 1) return null; double t = V3.Dot(e2, q) * inv; return t > 1e-12 ? t : null;} sealed class Mesh{ public readonly List<V3> V = new(); public readonly List<(int A, int B, int C)> F = new(); // counter-clockwise seen from outside public static Mesh Box(double lx, double ly, double lz) => Prism(new[] { (0.0, 0.0), (lx, 0.0), (lx, ly), (0.0, ly) }, lz); // A counter-clockwise footprint extruded up by h (floor and roof fanned from corner 0) public static Mesh Prism((double X, double Y)[] poly, double h) { var m = new Mesh(); int n = poly.Length; foreach (var (x, y) in poly) m.V.Add(new V3(x, y, 0)); foreach (var (x, y) in poly) m.V.Add(new V3(x, y, h)); for (int k = 1; k < n - 1; k++) { m.F.Add((0, k + 1, k)); // floor, facing down m.F.Add((n, n + k, n + k + 1)); // roof, facing up } for (int i = 0; i < n; i++) { int j = (i + 1) % n; m.F.Add((i, j, j + n)); // each wall as two triangles m.F.Add((i, j + n, i + n)); } return m; } public static Mesh Pyramid(double b, double h) { var m = new Mesh(); m.V.AddRange(new V3[] { new(0, 0, 0), new(b, 0, 0), new(b, b, 0), new(0, b, 0), new(b / 2, b / 2, h) }); m.F.AddRange(new[] { (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (0, 2, 1), (0, 3, 2) }); return m; } // (b - a) x (c - a): points out of the solid, and its length is twice the triangle's area public V3 Normal((int A, int B, int C) t) => V3.Cross(V[t.B] - V[t.A], V[t.C] - V[t.A]);} 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); static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###"); public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}(2, 1.5, 1): 1 crossing -> inside (5, 1, 1): 0 crossings -> outside (1, 1, 2.4): 1 crossing -> inside (2, 1.5, -0.5): 0 crossings -> outside
Remember
A ray O + t·d hits a triangle when t > 0 and the plane point is inside the triangle. Barycentric weights (w, u, v) add up to 1; the point is inside when all three are between 0 and 1. Möller–Trumbore finds t, u and v together; det = e₁ · (d × e₂) is zero for a parallel ray. For picking keep the smallest positive t; the weights also interpolate values such as terrain heights. Counting crossings tells inside from outside; edge hits need care, and big models need a BVH first.