Line segment-plane intersection accuracy

I have written a script that will compute the intersection point between a line segment and a plane. The algorithm to do this is quite simple and can be found easily anywhere on the internet.

I would like to use this with a plane facing in any direction that passes through two points on the edge of a box and find where it intersects with a line segment. An example of this is shown below (the plane passes through the two green points on the box).

The problem: Having found an intersection point, it should be possible to do a raycast back along the plane from the intersection point, and return a point on the edge of the box.

What happens in reality is the raycast just misses the edge of the box. The purple line is a visual representation of the raycast.
image

Since I found an intersection point between the plane and the line segment this SHOULD not be possible. I’m guessing this is some sort of floating point error, or a byproduct of dealing with such precise values, but what is the solution?

Below is a place with all the code and setup I am using to test this. Move the red parts to alter the line segment and the yellow part to change the projection of the plane. Just click ‘Run’ instead of ‘Play’.
planeIntersectionTesting.rbxl (20.6 KB)

I will include the code here as well.

local SMALL_NUM = 0.0001

-- Returns the normal of a plane from three points on the plane
-- Inputs: Three vectors of points on the plane.
function equation_plane(vec1, vec2, vec3) 
	local diff1 = vec2 - vec1
	local diff2 = vec3-vec1
	return diff1:Cross(diff2)
end

-- Create a point at the specified vector
function createVisualPoint(position)
	local visualPoint = Instance.new("Part")
	visualPoint.Name = "visualPoint"
	visualPoint.Anchored = true
	visualPoint.BrickColor = BrickColor.new("Lime green")
	visualPoint.Size = Vector3.new(0.2,0.2,0.2)
	visualPoint.CFrame = CFrame.new(position)
	visualPoint.Parent = workspace
	return visualPoint
end

-- Create an edge in space between point1 and point2
function createVisualEdge(point1, point2)
	local visualEdge = Instance.new("Part")
	visualEdge.Name = "visualEdge"
	visualEdge.Anchored = true
	visualEdge.BrickColor = BrickColor.new("Neon orange")
	local size = (point1-point2).magnitude
	visualEdge.Size = Vector3.new(0.1,0.1,size)
	visualEdge.CFrame = CFrame.new(point1, point2) * CFrame.new(0,0,-size/2)
	visualEdge.Parent = workspace
	return visualEdge
end

-- Create a visual plane defined by a vector3 point and normal.
function createVisualPlane(point, normal)
	local visualPlane = Instance.new("Part")
	visualPlane.Name = "visualPlane"
	visualPlane.Anchored = true
	visualPlane.Size = Vector3.new(20,20,0.1)
	visualPlane.CFrame = CFrame.new(point, point+normal)
	visualPlane.Transparency = 0.2
	visualPlane.Parent = workspace
	return visualPlane
end

-- Returns the intersection point of a line segment and a plane
-- Inputs
-- SP0 (vector3): A point on the line segment
-- SP1 (vector3): A point on the line segment
-- planePoint (vector3): A point on the plane
-- planeNormal (vector3): The normal vector to the plane
function intersectSegmentPlane(SP0, SP1, planePoint, planeNormal)
    local u = SP1 - SP0;
    local w = SP0 - planePoint;
    local D = planeNormal:Dot(u);
    local N = -(planeNormal:Dot(w))
    if (math.abs(D) < SMALL_NUM) then    -- segment is parallel to plane
        if (N == 0) then                     -- segment lies in plane
            return SP0;
        else
            return nil;                    -- no intersection
		end
    end
    -- they are not parallel
    -- compute intersect param
    local sI = N / D;
    if (sI < 0 or sI > 1) then
        return nil;                        -- no intersection
	end
   local I = SP0 + sI * u;                  -- compute segment intersect point
   return I;
end

local visualEdge
local visualPlane
local visualRaycast
local visualPoint1
local visualPoint2

game:GetService("RunService").Heartbeat:Connect(function()
	-- Cleanup visuals
	if visualEdge then visualEdge:Destroy() end
	if visualPlane then visualPlane:Destroy() end
	if visualRaycast then visualRaycast:Destroy() end
	if visualPoint1 then visualPoint1:Destroy() end
	if visualPoint2 then visualPoint2:Destroy() end
	
	
	local box = workspace.box
	
	-- Get the projection from planeProjection in workspace
	local planeProjection = workspace.planeProjection.Position
	
	-- Set two points on the plane to an edge on the box.
	local planePoint1 = (box.CFrame * CFrame.new(box.Size.x/2, box.Size.y/2, -box.Size.z/2)).p
	local planePoint2 = (box.CFrame * CFrame.new(box.Size.x/2, -box.Size.y/2, -box.Size.z/2)).p
	
	-- Set point 3 to the projection of the plane from planePoint2
	local planePoint3 = planePoint2 + planeProjection
	
	-- Define the two points that will make up the line segment
	local lineSegmentPoint1 = workspace.lineSegmentPoint1.Position
	local lineSegmentPoint2 = workspace.lineSegmentPoint2.Position
	
	-- Get the normal to the plane from the 3 points on the plane
	local normal = equation_plane(planePoint1, planePoint2, planePoint3)

	-- Get the intersetion point between the plane and the line segment
	local intersection = intersectSegmentPlane(lineSegmentPoint1, lineSegmentPoint2, planePoint1, normal)
	
	if intersection then
		
		-- Raycast back from the intersection in the opposite direction of the plane's projection.
		-- This raycast SHOULD always return a hit because a hit was found between the line segment and the plane. 
		local ray = Ray.new(intersection, -planeProjection)
		local part, position = workspace:FindPartOnRayWithWhitelist(ray, {workspace.box})
		
		-- Visually display the raycast (it will be evident that the raycast misses the box, WHYYY??)
		visualRaycast = createVisualEdge(position, intersection)
		visualRaycast.BrickColor = BrickColor.new("Hot pink")
	end
	
	-- Create visuals.
	visualPoint1 = createVisualPoint(planePoint1)
	visualPoint2 = createVisualPoint(planePoint2)
	visualEdge = createVisualEdge(lineSegmentPoint1, lineSegmentPoint2)
	visualPlane = createVisualPlane(planePoint1, normal)
end)

There’s no issue I can see with your Line-Plane intersection function. It’s likely just a precision error, as changing:

local planePoint1 = (box.CFrame * CFrame.new(box.Size.x/2, box.Size.y/2, -box.Size.z/2)).p
local planePoint2 = (box.CFrame * CFrame.new(box.Size.x/2, -box.Size.y/2, -box.Size.z/2)).p

to

local planePoint1 = (box.CFrame * CFrame.new(box.Size.x/2.0001, box.Size.y/2.0001, -box.Size.z/2.0001)).p
local planePoint2 = (box.CFrame * CFrame.new(box.Size.x/2.0001, -box.Size.y/2.0001, -box.Size.z/2.0001)).p

will always return a hit with the raycast. Unless you REALLY need the precision of exactly the corner of the box, just adding a small epsilon to the divisor should work alright.

1 Like

Yep, makes sense. I don’t know why I didn’t think of doing that tbh. Thank you for the help!!!

1 Like