Is it possible to get all parts intersecting a Ray?

Title?

Since workspace:FindPartOnRay() only returns the first part

1 Like

There is not a built-in way, but you can just use FindPartOnRayWithIgnoreList over and over until nothing gets returned, adding the returned part to the ignore list every time:

local function GetAllParts(Ray)
    local Parts = {}
    local LastPart
    
    repeat
        LastPart = workspace:FindPartOnRayWithIgnoreList(Ray, Parts)
        table.insert(Parts, LastPart)
    until LastPart == nil

    return Parts
end
11 Likes

While this is the correct answer and the best way to do this…

Technically there is. You can use it on any camera as long as the camera is under Game.

local getBetweenParts, getBetweenPartsMultiple
do
	local camera = Instance.new("Camera", script)
	local emptyIgnoreList = {}
	local arr = {}
	function getBetweenParts(a, b, ignore)
		if not camera.Parent then
			camera = Instance.new("Camera", script)
		end
		camera.CFrame = CFrame.new(a)
		arr[1] = b
		return camera:GetPartsObscuringTarget(arr, ignore or emptyIgnoreList)
	end
	function getBetweenPartsMultiple(a, b, ignore)
		if not camera.Parent then
			camera = Instance.new("Camera", script)
		end
		camera.CFrame = CFrame.new(a)
		return camera:GetPartsObscuringTarget(b, ignore or emptyIgnoreList)
	end
end

I imagine this might be insignificantly more performant than using raycasts in most cases, so you should really just use raycasts. Still neat to know about, though! This camera method wasn’t meant to be used this way so I’d call it a hack.

If you put this functionality behind a module then you can switch to this camera hack mode if you run into performance issues, or you can switch out of it if it stops working because it’s a hack.

9 Likes

I think a more efficient way to do would be to make a new raycast from the contact point of the previous one, and repeat. Only caveat is that there’s a chance you’ll get the same part twice if you have any concave hitboxes on your meshes or unions.

2 Likes

Combine this with an ignore list - problem solved!

2 Likes

This function, GetPartsObscuringTarget, used to be implemented as a repeating raycast loop, that built up an ignorelist. It is a special function created for Invisicam. It scaled poorly with the number of parts hit (worse than quadratic), and was the source of low framerate issues with Invisicam, particularly in part-dense places and long camera-to-subject distance. So, last summer, I reimplemented it internally, as a parts-intersecting-line-segment check. For the pathological cases, the speed improvement was > 1000x. For the simple cases of there being just a dozen or so parts between the camera and character, performance is only negligibly faster. The new way is also a lot faster when there is a big ignore list, since things like characters and models with lots of parts get traversed just once (rather than once per raycast).

The tradeoff for the way it’s optimized, is that the parts are not returned with any of the usual ray cast data (hit point, normal, mat), and the list of parts is in a fairly arbitrary order, not in the order that a ray would encounter them.

That said, it does the one thing it can do very efficiently. There are certainly circumstances where calling this once, and knowing everything that a ray along this path will hit, can shave a lot of time off of building up an ignorelist or whitelist, and in many cases can allow you to follow up with a ray that has a single part whitelisted, just to get the hitPos, normal or material. A short whitelist is WAY more efficient than a big ignorelist.

And before you ask, it’s on my to-do list to get a general purpose piercing raycast into the API.

And while it is a hack, the overhead of a Camera instance that is not the CurrentCamera is now very light. The way you’re using it shouldn’t cause any performance issues today.

11 Likes

Just to check, this should return the parts in order from closest to furthest, right? Personally I don’t have any cases l would use it for unless it returns them in order, which I imagine it will.

It would return the hit points ordered from closest to farthest from the ray origin. Parts would therefore be in the results list in the order hit, and a concave part could be in the list multiple times. If we include ‘exit’ points, all parts will be in the list at least twice except for those the ray starts or ends inside (though if the ray both starts and ends in the same part, it can also be in the list twice if it’s concave and the ray exits and re-enters it, e.g. a ray between 2 points inside the “ends” of a C-shaped part).

1 Like

I have a GJK distance and intersection implementation. That should work until a c++ implementation is made. I ran into issues like this years ago (particularly when working on my navigation mesh pathfinder).

Below is pretty much all the code you need. You could remove the spherical check, since the maximum number of parts FindPartsInRegion3 returns is relatively small. I’d recommend replacing FindPartsInRegion3 with my AABB structure if the parts wont be moving around too much.


--< BEWARE >--
-- untested code ahead --

local GJK = require 'GJK'

local min = math.min
local max = math.max

local function rejectionVector(point, start, dir)
	local vStartToPoint = point - start
	return vStartToPoint - vStartToPoint:Dot(dir) * dir
end

local function rejectionMagnitudeSquared(point, start, dir)
	local vStartToPoint = point - start
	local projectionMagnitude = vStartToPoint:Dot(dir)
	return vStartToPoint:Dot(vStartPoint) - projectionMagnitude * projectionMagnitude
end

local pPos, pI, pJ, pK
local function supportPart(dir)
	return pPos
		+ pI:Dot(dir) > 0 and pI or -pI
		+ pJ:Dot(dir) > 0 and pJ or -pJ
		+ pK:Dot(dir) > 0 and pK or -pK
end

local rayStart, rayFinish
local function supportRay(dir)
	if rayStart:Dot(dir) > rayFinish:Dot(dir) then
		return rayStart
	else
		return rayFinish
	end
end

local function findPartsOnRay(start, dir, maxDist)
	finish = start + dir * maxDist
	-- Alternatively you could provide your own data structure
	-- to minimize the number of tested parts. I have a AABB spatial
	-- structure here: https://github.com/TylerRHoyer/AABB-Octree

	-- Also alternatively, you could provide your own set of parts to test
	-- if you only want to test a couple parts.

	-- To optimize this, you could do more, smaller region checks
	-- closer to the ray.
	local minP = 
	local maxP = 
	local parts = workspace:FindPartsInRegion3(
		Region3.new(
			Vector3.new(
				min(start.x, finish.x),
				min(start.y, finish.y),
				min(start.z, finish.z)
			), 
			Vector3.new(
				max(start.x, finish.x),
				max(start.y, finish.y),
				max(start.z, finish.z)
			)
		),
		nil,
		100 -- I'm not sure what the largest maximum number of parts returned is
	)

	-- This check is optional, but may reduce the number of parts that
	-- go into the next more expensive operation.
	for i, part in next, parts do
		if rejectionMagnitudeSquared(part.Position, start, dir) <= part.Size:Dot(part.Size) then
			parts[i] = nil
		end
	end

	-- You can combine this loop with the above, but I seperated to show
	-- the distinction between the operations.
	rayStart = start
	rayFinish = finish
	for i, part in next, parts do
		local cf = part.CFrame
		cf = cf - cf.p
		local s = part.Size * 0.5

		pPos = part.Position
		pI = cf.RightVector * s.X
		pJ = cf.UpVector * s.Y
		pK = cf.LookVector * s.Z
		if not GJK.intersection(
			supportPart,
			supportRay,
			rejectionVector(pPos, start, dir)
		) then
			parts[i] = nil
		end
	end

	return parts
end

Basically the core of the code is the call to GJK.intersection. It sets two callbacks, which when called with a vector return the point in the set they represent furthest in that direction. (As a side note, to represent a sphere you wouldn’t need a large number of points, just the center and radius. You’d return the center plus the direction times the radius. Now THAT is slick!) The third parameter is the starting search direction. It returns true if intersecting, and false if not.

Edit: Did I just necro this? Huh… my filters were set onto ‘latest’…

9 Likes