Hi everyone. A while back I was playing around with the following problem:
Given four points in 3D space, how can I find the sphere touching each of those points (if it exists)?
Since this problem isn’t always possible to solve, it may be tricky to deal with the cases cleanly.
I first wrote out a solution on paper, and then I went to implement it in Roblox.
I ended up finding a way to do it that uses CFrame in a “hacky” way.
I’ll eventually try to explain what I did here. But first let me just show you all what I did!
Watch how the code deals with singularities:
ModuleScript
local module = {}
function module:InvertMatrix3D(matrix: CFrame): CFrame
local _, _, _, a, b, c, d, e, f, g, h, i = matrix:GetComponents()
local adjugate = CFrame.new(
0, 0, 0,
e * i - f * h,
c * h - b * i,
b * f - c * e,
f * g - d * i,
a * i - c * g,
c * d - a * f,
d * h - e * g,
b * g - a * h,
a * e - b * d
)
local det = matrix:Inverse().XVector:Dot(adjugate.XVector)
return CFrame.fromMatrix(
Vector3.zero,
adjugate.XVector / det,
adjugate.YVector / det,
adjugate.ZVector / det
)
end
function module:GetCircumcenter3D(point0: Vector3, point1: Vector3, point2: Vector3, point3: Vector3): Vector3
local matrix = CFrame.fromMatrix(
Vector3.zero, -- fourth column
point1 - point0, -- first column
point2 - point0, -- second column
point3 - point0 -- third column
)
local inverse = module:InvertMatrix3D(matrix:Inverse()) -- get the inverse of the transpose of the matrix
local dotProducts = Vector3.new(point1:Dot(point1), point2:Dot(point2), point3:Dot(point3))
local vector = (dotProducts - point0:Dot(point0) * Vector3.one) / 2
return inverse * vector
end
return module
Example use
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Utilities = require(ReplicatedStorage.Common.BoundingSphere)
local quad: {BasePart} = workspace.Quad:GetChildren()
local part0, part1, part2, part3 = unpack(quad)
local circumcenter = workspace.Circumcenter
local ball = workspace.Ball
RunService.Heartbeat:Connect(function()
circumcenter.Position = Utilities:GetCircumcenter3D(part0.Position, part1.Position, part2.Position, part3.Position)
ball.Position = circumcenter.Position
ball.Size = 2 * Vector3.one * (ball.Position - part0.Position).Magnitude
end)
The original way which was even hackier and somehow slower
Click to show...
local RunService = game:GetService("RunService")
local quad : {BasePart} = workspace.Quad:GetChildren()
local part0, part1, part2, part3 = quad[1], quad[2], quad[3], quad[4]
local function getRotation(cf: CFrame): string
local _, __, __, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cf:components()
local str = string.format(string.rep("%.2f, ", 9), r00, r01, r02, r10, r11, r12, r20, r21, r22)
str = string.gsub(str, "%-?%d+%.%d+, %-?%d+%.%d+, %-?%d+%.%d+, ", function(s)
return s .. "\n"
end)
str = string.gsub(str, ",", "")
return str
end
local function getTrace(matrix: CFrame)
return matrix.XVector.X + matrix.YVector.Y + matrix.ZVector.Z
end
local function invertMatrix3D(matrix: CFrame): CFrame
local squared = matrix * matrix
local trace = getTrace(matrix)
local diff = 0.5 * (trace^2 - getTrace(squared))
local adjugate = CFrame.fromMatrix(
Vector3.zero,
Vector3.xAxis * diff - trace * matrix.XVector + squared.XVector,
Vector3.yAxis * diff - trace * matrix.YVector + squared.YVector,
Vector3.zAxis * diff - trace * matrix.ZVector + squared.ZVector
)
local det = matrix.XVector.X * adjugate.XVector.X
det += matrix.YVector.X * adjugate.XVector.Y
det += matrix.ZVector.X * adjugate.XVector.Z
return CFrame.fromMatrix(
Vector3.zero,
adjugate.XVector / det,
adjugate.YVector / det,
adjugate.ZVector / det
)
end
local function getCircumcenter3D(point0: Vector3, point1: Vector3, point2: Vector3, point3: Vector3): Vector3
local matrix = CFrame.fromMatrix(
Vector3.zero, -- fourth column
2 * (point1 - point0), -- first column
2 * (point2 - point0), -- second column
2 * (point3 - point0) -- third column
)
local inverse = invertMatrix3D(matrix:Inverse()) -- :Inverse() transposes the matrix
local dotProducts = Vector3.new(
point1:Dot(point1),
point2:Dot(point2),
point3:Dot(point3)
)
local vector = dotProducts - point0:Dot(point0) * Vector3.one
print("Matrix\n", getRotation(matrix))
print("Inverse\n", getRotation(inverse))
print("Vector", vector)
return inverse * vector
end
local circumcenter = workspace.Circumcenter
local ball = workspace.Ball
RunService.Heartbeat:Connect(function()
circumcenter.Position = circumcenter.Position:Lerp(getCircumcenter3D(part0.Position, part1.Position, part2.Position, part3.Position), 0.05)
ball.Position = ball.Position:Lerp(circumcenter.Position, 0.05)
ball.Size = ball.Size:Lerp(2 * Vector3.one * (ball.Position - part0.Position).Magnitude, 0.05)
end)
The 2D solution
I also wrote some (less optimized) code to solve for the circumcenter of a triangle (three points).
Click to show...
local RunService = game:GetService("RunService")
local triangle: {BasePart} = workspace.Triangle:GetChildren()
local part0, part1, part2 = triangle[1], triangle[2], triangle[3]
local projectXZ = CFrame.fromMatrix(
Vector3.zero,
Vector3.xAxis,
Vector3.zero,
Vector3.yAxis
)
local permuteYZ = CFrame.fromMatrix(
Vector3.zero,
Vector3.xAxis,
Vector3.zAxis,
Vector3.yAxis
)
local function invertXYMatrix(matrix: CFrame)
local matrixX, matrixY = matrix.XVector, matrix.YVector
local a, b, c, d = matrixX.X, matrixY.X, matrixX.Y, matrixY.Y
local det = a * d - b * c
return CFrame.fromMatrix(
Vector3.zero,
Vector3.new(d, -c, 0) / det,
Vector3.new(-b, a, 0) / det,
Vector3.zero
)
end
local function getCircumcenter2D(point0: Vector3, point1: Vector3, point2: Vector3)
local projected = projectXZ * CFrame.fromMatrix(Vector3.zero, point0, point1, point2)
local projX, projY, projZ = projected.XVector, projected.YVector, projected.ZVector
local matrix = CFrame.fromMatrix(
Vector3.zero, -- fourth column
2 * (projY - projX), -- first column
2 * (projZ - projX), -- second column
Vector3.zero -- third column
)
local inverse = invertXYMatrix(matrix:Inverse())
local dotX = projX:Dot(projX)
local dotYZ = Vector3.new(
projY:Dot(projY),
projZ:Dot(projZ),
dotX
)
local vector = dotYZ - dotX * Vector3.one
return inverse * vector
end
local circumcenter = workspace.Circumcenter
RunService.Heartbeat:Connect(function()
circumcenter.Position = permuteYZ * getCircumcenter2D(part0.Position, part1.Position, part2.Position) + Vector3.yAxis
end)
Let me know what you think! Especially if you have any ways it could be improved.