Geometry.kt
src/jvmMain/kotlin/afield/math/linalg/Geometry.kt · 429 lines
package afield.math.linalg
Closest-point, containment and intersection tests for segments, triangles, boxes, spheres and rays.
Each function takes read-only vectors and writes its answer into a caller-supplied dest, or
returns a Boolean or distance, so none of them allocates. Boxes are axis-aligned and given as a
minimum and maximum corner. Segment and triangle functions handle a zero-length segment or a
collapsed triangle explicitly instead of dividing by zero.
afield.math.linalg.arena has allocation-free versions of some of these that work on arena
vectors inside frameScratch.
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
fun closestPointOnSegment(point: Vector2fc, a: Vector2fc, b: Vector2fc, dest: Vector2f): Vector2f
Closest point on the finite line segment [a, b]. Safe for a zero-length segment.
fun closestPointOnSegment(point: Vector2fc, a: Vector2fc, b: Vector2fc, dest: Vector2f): Vector2f {
val abx = b.x() - a.x()
val aby = b.y() - a.y()
val denominator = abx * abx + aby * aby
val t = if (denominator == 0f) 0f else
((point.x() - a.x()) * abx + (point.y() - a.y()) * aby) / denominator
val clamped = t.coerceIn(0f, 1f)
return dest.set(a.x() + abx * clamped, a.y() + aby * clamped)
}
fun closestPointOnSegment(point: Vector3fc, a: Vector3fc, b: Vector3fc, dest: Vector3f): Vector3f
Closest point on the finite line segment [a, b]. Safe for a zero-length segment.
fun closestPointOnSegment(point: Vector3fc, a: Vector3fc, b: Vector3fc, dest: Vector3f): Vector3f {
val abx = b.x() - a.x()
val aby = b.y() - a.y()
val abz = b.z() - a.z()
val denominator = abx * abx + aby * aby + abz * abz
val t = if (denominator == 0f) 0f else
((point.x() - a.x()) * abx + (point.y() - a.y()) * aby + (point.z() - a.z()) * abz) / denominator
val clamped = t.coerceIn(0f, 1f)
return dest.set(a.x() + abx * clamped, a.y() + aby * clamped, a.z() + abz * clamped)
}
fun closestPointsBetweenSegments
Writes the closest pair on segments [a0, a1] and [b0, b1], returning their squared distance.
Degenerate point-segments are supported and both destinations may alias input heap vectors.
fun closestPointsBetweenSegments(
a0: Vector3fc,
a1: Vector3fc,
b0: Vector3fc,
b1: Vector3fc,
destA: Vector3f,
destB: Vector3f,
epsilon: Float = 1e-12f,
): Float {
val d1x = a1.x() - a0.x(); val d1y = a1.y() - a0.y(); val d1z = a1.z() - a0.z()
val d2x = b1.x() - b0.x(); val d2y = b1.y() - b0.y(); val d2z = b1.z() - b0.z()
val rx = a0.x() - b0.x(); val ry = a0.y() - b0.y(); val rz = a0.z() - b0.z()
val a = d1x * d1x + d1y * d1y + d1z * d1z
val e = d2x * d2x + d2y * d2y + d2z * d2z
val f = d2x * rx + d2y * ry + d2z * rz
var s: Float
var t: Float
if (a <= epsilon && e <= epsilon) {
s = 0f; t = 0f
} else if (a <= epsilon) {
s = 0f; t = (f / e).coerceIn(0f, 1f)
} else {
val c = d1x * rx + d1y * ry + d1z * rz
if (e <= epsilon) {
t = 0f; s = (-c / a).coerceIn(0f, 1f)
} else {
val b = d1x * d2x + d1y * d2y + d1z * d2z
val denominator = a * e - b * b
s = if (abs(denominator) > epsilon) ((b * f - c * e) / denominator).coerceIn(0f, 1f) else 0f
t = (b * s + f) / e
if (t < 0f) { t = 0f; s = (-c / a).coerceIn(0f, 1f) }
else if (t > 1f) { t = 1f; s = ((b - c) / a).coerceIn(0f, 1f) }
}
}
val ax = a0.x() + d1x * s; val ay = a0.y() + d1y * s; val az = a0.z() + d1z * s
val bx = b0.x() + d2x * t; val by = b0.y() + d2y * t; val bz = b0.z() + d2z * t
destA.set(ax, ay, az); destB.set(bx, by, bz)
val dx = ax - bx; val dy = ay - by; val dz = az - bz
return dx * dx + dy * dy + dz * dz
}
fun intersectSegments
Single-point 2D segment intersection. Collinear overlap returns false because it has no unique point.
fun intersectSegments(
a0: Vector2fc,
a1: Vector2fc,
b0: Vector2fc,
b1: Vector2fc,
dest: Vector2f,
epsilon: Float = 1e-7f,
): Boolean {
val adx = a1.x() - a0.x(); val ady = a1.y() - a0.y()
val bdx = b1.x() - b0.x(); val bdy = b1.y() - b0.y()
val denominator = adx * bdy - ady * bdx
if (abs(denominator) <= epsilon) return false
val rx = b0.x() - a0.x(); val ry = b0.y() - a0.y()
val t = (rx * bdy - ry * bdx) / denominator
val u = (rx * ady - ry * adx) / denominator
if (t !in 0f..1f || u !in 0f..1f) return false
dest.set(a0.x() + adx * t, a0.y() + ady * t)
return true
}
fun barycentricCoordinates
Writes barycentric coordinates (u, v, w) for point point relative to triangle a, b, c.
Returns false for a degenerate triangle and leaves dest unchanged.
fun barycentricCoordinates(
point: Vector3fc,
a: Vector3fc,
b: Vector3fc,
c: Vector3fc,
dest: Vector3f,
epsilon: Float = 1e-12f,
): Boolean {
val v0x = b.x() - a.x(); val v0y = b.y() - a.y(); val v0z = b.z() - a.z()
val v1x = c.x() - a.x(); val v1y = c.y() - a.y(); val v1z = c.z() - a.z()
val v2x = point.x() - a.x(); val v2y = point.y() - a.y(); val v2z = point.z() - a.z()
val d00 = v0x * v0x + v0y * v0y + v0z * v0z
val d01 = v0x * v1x + v0y * v1y + v0z * v1z
val d11 = v1x * v1x + v1y * v1y + v1z * v1z
val d20 = v2x * v0x + v2y * v0y + v2z * v0z
val d21 = v2x * v1x + v2y * v1y + v2z * v1z
val denominator = d00 * d11 - d01 * d01
if (abs(denominator) <= epsilon) return false
val v = (d11 * d20 - d01 * d21) / denominator
val w = (d00 * d21 - d01 * d20) / denominator
return dest.set(1f - v - w, v, w).let { true }
}
fun containsPointInTriangle
fun containsPointInTriangle(
point: Vector3fc,
a: Vector3fc,
b: Vector3fc,
c: Vector3fc,
tolerance: Float = 1e-6f,
): Boolean {
// Expanded barycentric calculation avoids allocating a temporary vector.
val v0x = b.x() - a.x(); val v0y = b.y() - a.y(); val v0z = b.z() - a.z()
val v1x = c.x() - a.x(); val v1y = c.y() - a.y(); val v1z = c.z() - a.z()
val v2x = point.x() - a.x(); val v2y = point.y() - a.y(); val v2z = point.z() - a.z()
val d00 = v0x * v0x + v0y * v0y + v0z * v0z
val d01 = v0x * v1x + v0y * v1y + v0z * v1z
val d11 = v1x * v1x + v1y * v1y + v1z * v1z
val d20 = v2x * v0x + v2y * v0y + v2z * v0z
val d21 = v2x * v1x + v2y * v1y + v2z * v1z
val denominator = d00 * d11 - d01 * d01
if (abs(denominator) <= 1e-12f) return false
val nx = v0y * v1z - v0z * v1y
val ny = v0z * v1x - v0x * v1z
val nz = v0x * v1y - v0y * v1x
val planeNumerator = v2x * nx + v2y * ny + v2z * nz
if (planeNumerator * planeNumerator > tolerance * tolerance * (nx * nx + ny * ny + nz * nz)) return false
val v = (d11 * d20 - d01 * d21) / denominator
val w = (d00 * d21 - d01 * d20) / denominator
val u = 1f - v - w
return u >= -tolerance && v >= -tolerance && w >= -tolerance
}
fun closestPointOnTriangle
Ericson-style closest point on a triangle, including degenerate edge regions.
body — 82 lines, from line 166
fun closestPointOnTriangle(
point: Vector3fc,
a: Vector3fc,
b: Vector3fc,
c: Vector3fc,
dest: Vector3f,
): Vector3f {
val abx = b.x() - a.x(); val aby = b.y() - a.y(); val abz = b.z() - a.z()
val acx = c.x() - a.x(); val acy = c.y() - a.y(); val acz = c.z() - a.z()
val crossX = aby * acz - abz * acy
val crossY = abz * acx - abx * acz
val crossZ = abx * acy - aby * acx
if (crossX * crossX + crossY * crossY + crossZ * crossZ <= 1e-20f) {
return closestPointOnDegenerateTriangle(point, a, b, c, dest)
}
val apx = point.x() - a.x(); val apy = point.y() - a.y(); val apz = point.z() - a.z()
val d1 = abx * apx + aby * apy + abz * apz
val d2 = acx * apx + acy * apy + acz * apz
if (d1 <= 0f && d2 <= 0f) return dest.set(a)
val bpx = point.x() - b.x(); val bpy = point.y() - b.y(); val bpz = point.z() - b.z()
val d3 = abx * bpx + aby * bpy + abz * bpz
val d4 = acx * bpx + acy * bpy + acz * bpz
if (d3 >= 0f && d4 <= d3) return dest.set(b)
val vc = d1 * d4 - d3 * d2
if (vc <= 0f && d1 >= 0f && d3 <= 0f) {
val v = d1 / (d1 - d3)
return dest.set(a.x() + v * abx, a.y() + v * aby, a.z() + v * abz)
}
val cpx = point.x() - c.x(); val cpy = point.y() - c.y(); val cpz = point.z() - c.z()
val d5 = abx * cpx + aby * cpy + abz * cpz
val d6 = acx * cpx + acy * cpy + acz * cpz
if (d6 >= 0f && d5 <= d6) return dest.set(c)
val vb = d5 * d2 - d1 * d6
if (vb <= 0f && d2 >= 0f && d6 <= 0f) {
val w = d2 / (d2 - d6)
return dest.set(a.x() + w * acx, a.y() + w * acy, a.z() + w * acz)
}
val va = d3 * d6 - d5 * d4
if (va <= 0f && d4 - d3 >= 0f && d5 - d6 >= 0f) {
val bcx = c.x() - b.x(); val bcy = c.y() - b.y(); val bcz = c.z() - b.z()
val w = (d4 - d3) / ((d4 - d3) + (d5 - d6))
return dest.set(b.x() + w * bcx, b.y() + w * bcy, b.z() + w * bcz)
}
val denominator = va + vb + vc
val inverse = 1f / denominator
val v = vb * inverse
val w = vc * inverse
return dest.set(
a.x() + abx * v + acx * w,
a.y() + aby * v + acy * w,
a.z() + abz * v + acz * w,
)
}
private fun closestPointOnDegenerateTriangle(
point: Vector3fc,
a: Vector3fc,
b: Vector3fc,
c: Vector3fc,
dest: Vector3f,
): Vector3f {
closestPointOnSegment(point, a, b, dest)
var bestX = dest.x; var bestY = dest.y; var bestZ = dest.z
var bestDistance = dest.distanceSquared(point)
closestPointOnSegment(point, b, c, dest)
var distance = dest.distanceSquared(point)
if (distance < bestDistance) {
bestDistance = distance; bestX = dest.x; bestY = dest.y; bestZ = dest.z
}
closestPointOnSegment(point, c, a, dest)
distance = dest.distanceSquared(point)
if (distance < bestDistance) {
bestX = dest.x; bestY = dest.y; bestZ = dest.z
}
return dest.set(bestX, bestY, bestZ)
}
fun closestPointOnAabb(point: Vector3fc, minimum: Vector3fc, maximum: Vector3fc, dest: Vector3f): Vector3f
fun closestPointOnAabb(point: Vector3fc, minimum: Vector3fc, maximum: Vector3fc, dest: Vector3f): Vector3f =
dest.set(
point.x().coerceIn(minimum.x(), maximum.x()),
point.y().coerceIn(minimum.y(), maximum.y()),
point.z().coerceIn(minimum.z(), maximum.z()),
)
fun containsPointAabb(point: Vector3fc, minimum: Vector3fc, maximum: Vector3fc): Boolean
fun containsPointAabb(point: Vector3fc, minimum: Vector3fc, maximum: Vector3fc): Boolean =
point.x() in minimum.x()..maximum.x() &&
point.y() in minimum.y()..maximum.y() &&
point.z() in minimum.z()..maximum.z()
fun intersectsAabb(aMin: Vector3fc, aMax: Vector3fc, bMin: Vector3fc, bMax: Vector3fc): Boolean
fun intersectsAabb(aMin: Vector3fc, aMax: Vector3fc, bMin: Vector3fc, bMax: Vector3fc): Boolean =
aMax.x() >= bMin.x() && aMin.x() <= bMax.x() &&
aMax.y() >= bMin.y() && aMin.y() <= bMax.y() &&
aMax.z() >= bMin.z() && aMin.z() <= bMax.z()
fun containsPointSphere(point: Vector3fc, center: Vector3fc, radius: Float): Boolean
fun containsPointSphere(point: Vector3fc, center: Vector3fc, radius: Float): Boolean =
point.distanceSquared(center) <= radius * radius
fun intersectsSphere(aCenter: Vector3fc, aRadius: Float, bCenter: Vector3fc, bRadius: Float): Boolean
fun intersectsSphere(aCenter: Vector3fc, aRadius: Float, bCenter: Vector3fc, bRadius: Float): Boolean {
val radius = aRadius + bRadius
return aCenter.distanceSquared(bCenter) <= radius * radius
}
fun intersectsAabbSphere(minimum: Vector3fc, maximum: Vector3fc, center: Vector3fc, radius: Float): Boolean
fun intersectsAabbSphere(minimum: Vector3fc, maximum: Vector3fc, center: Vector3fc, radius: Float): Boolean {
val dx = when { center.x() < minimum.x() -> minimum.x() - center.x(); center.x() > maximum.x() -> center.x() - maximum.x(); else -> 0f }
val dy = when { center.y() < minimum.y() -> minimum.y() - center.y(); center.y() > maximum.y() -> center.y() - maximum.y(); else -> 0f }
val dz = when { center.z() < minimum.z() -> minimum.z() - center.z(); center.z() > maximum.z() -> center.z() - maximum.z(); else -> 0f }
return dx * dx + dy * dy + dz * dz <= radius * radius
}
fun rayTriangleDistance
Returns the forward ray distance, or Float.NaN when the ray misses or the triangle is degenerate.
fun rayTriangleDistance(
origin: Vector3fc,
direction: Vector3fc,
a: Vector3fc,
b: Vector3fc,
c: Vector3fc,
epsilon: Float = 1e-7f,
): Float {
val e1x = b.x() - a.x(); val e1y = b.y() - a.y(); val e1z = b.z() - a.z()
val e2x = c.x() - a.x(); val e2y = c.y() - a.y(); val e2z = c.z() - a.z()
val px = direction.y() * e2z - direction.z() * e2y
val py = direction.z() * e2x - direction.x() * e2z
val pz = direction.x() * e2y - direction.y() * e2x
val determinant = e1x * px + e1y * py + e1z * pz
if (abs(determinant) <= epsilon) return Float.NaN
val inverse = 1f / determinant
val tx = origin.x() - a.x(); val ty = origin.y() - a.y(); val tz = origin.z() - a.z()
val u = (tx * px + ty * py + tz * pz) * inverse
if (u < 0f || u > 1f) return Float.NaN
val qx = ty * e1z - tz * e1y
val qy = tz * e1x - tx * e1z
val qz = tx * e1y - ty * e1x
val v = (direction.x() * qx + direction.y() * qy + direction.z() * qz) * inverse
if (v < 0f || u + v > 1f) return Float.NaN
val distance = (e2x * qx + e2y * qy + e2z * qz) * inverse
return if (distance >= 0f) distance else Float.NaN
}
fun intersectRayTriangle
fun intersectRayTriangle(
origin: Vector3fc,
direction: Vector3fc,
a: Vector3fc,
b: Vector3fc,
c: Vector3fc,
dest: Vector3f,
epsilon: Float = 1e-7f,
): Boolean {
val distance = rayTriangleDistance(origin, direction, a, b, c, epsilon)
if (distance.isNaN()) return false
dest.set(
origin.x() + direction.x() * distance,
origin.y() + direction.y() * distance,
origin.z() + direction.z() * distance,
)
return true
}
fun intersectRayPlane
fun intersectRayPlane(
origin: Vector3fc,
direction: Vector3fc,
pointOnPlane: Vector3fc,
normal: Vector3fc,
dest: Vector3f,
epsilon: Float = 1e-7f,
): Boolean {
val denominator = direction.dot(normal)
if (abs(denominator) <= epsilon) return false
val t = ((pointOnPlane.x() - origin.x()) * normal.x() +
(pointOnPlane.y() - origin.y()) * normal.y() +
(pointOnPlane.z() - origin.z()) * normal.z()) / denominator
if (t < 0f) return false
dest.set(origin.x() + direction.x() * t, origin.y() + direction.y() * t, origin.z() + direction.z() * t)
return true
}
fun intersectRaySphere
Writes forward (near, far) ray distances. Direction need not be normalized.
fun intersectRaySphere(
origin: Vector3fc,
direction: Vector3fc,
center: Vector3fc,
radius: Float,
nearFar: Vector2f,
epsilon: Float = 1e-12f,
): Boolean {
val mx = origin.x() - center.x(); val my = origin.y() - center.y(); val mz = origin.z() - center.z()
val a = direction.lengthSquared()
if (a <= epsilon) return false
val b = mx * direction.x() + my * direction.y() + mz * direction.z()
val c = mx * mx + my * my + mz * mz - radius * radius
val discriminant = b * b - a * c
if (discriminant < 0f) return false
val root = sqrt(discriminant)
val near = (-b - root) / a
val far = (-b + root) / a
if (far < 0f) return false
nearFar.set(max(0f, near), far)
return true
}
fun intersectRayAabb
Writes forward (near, far) ray distances.
fun intersectRayAabb(
origin: Vector3fc,
direction: Vector3fc,
minimum: Vector3fc,
maximum: Vector3fc,
nearFar: Vector2f,
epsilon: Float = 1e-12f,
): Boolean = intersectParametricAabb(origin, direction, minimum, maximum, 0f, Float.POSITIVE_INFINITY, nearFar, epsilon)
fun intersectSegmentAabb
Writes segment parameters (near, far) in [0, 1].
body — 47 lines, from line 382
fun intersectSegmentAabb(
start: Vector3fc,
end: Vector3fc,
minimum: Vector3fc,
maximum: Vector3fc,
nearFar: Vector2f,
epsilon: Float = 1e-12f,
): Boolean {
val dx = end.x() - start.x(); val dy = end.y() - start.y(); val dz = end.z() - start.z()
nearFar.set(0f, 1f)
return clipSlab(start.x(), dx, minimum.x(), maximum.x(), nearFar, epsilon) &&
clipSlab(start.y(), dy, minimum.y(), maximum.y(), nearFar, epsilon) &&
clipSlab(start.z(), dz, minimum.z(), maximum.z(), nearFar, epsilon)
}
private fun intersectParametricAabb(
origin: Vector3fc,
direction: Vector3fc,
minimum: Vector3fc,
maximum: Vector3fc,
initialNear: Float,
initialFar: Float,
nearFar: Vector2f,
epsilon: Float,
): Boolean {
nearFar.set(initialNear, initialFar)
return clipSlab(origin.x(), direction.x(), minimum.x(), maximum.x(), nearFar, epsilon) &&
clipSlab(origin.y(), direction.y(), minimum.y(), maximum.y(), nearFar, epsilon) &&
clipSlab(origin.z(), direction.z(), minimum.z(), maximum.z(), nearFar, epsilon)
}
private fun clipSlab(
origin: Float,
direction: Float,
low: Float,
high: Float,
nearFar: Vector2f,
epsilon: Float,
): Boolean {
if (abs(direction) <= epsilon) return origin in low..high
var a = (low - origin) / direction
var b = (high - origin) / direction
if (a > b) { val swap = a; a = b; b = swap }
nearFar.x = max(nearFar.x, a)
nearFar.y = min(nearFar.y, b)
return nearFar.x <= nearFar.y
}