workspace:FindPartOnRayWithCallback(Ray ray, Function callback)

As a Roblox developer, it’s currently kind of awkward writing our own raycasting conditions. If our raycast hits a part that we want to ignore, we have to add it to an ignore list and then do the raycast again until the conditions we’d like to meet are satisfied. I think a more generic solution that uses a function callback would be handy.

Thus, I would like to propose FindPartOnRayWithCallback.
How it would work:

local point0 = Vector3.new(5,5,5)
local point1 = Vector3.new(20,20,20)

local ray = Ray.new(point0, point1-point0)

local hit,pos,norm,mat = workspace:FindPartOnRayWithCallback(ray, function (hit)
	if hit.Transparency > 0.95 or not hit.CanCollide then
		return Enum.RayCallbackResult.Ignore -- Continue the raycast, and ignore this part from now on.
	elseif hit.Name == "BlockRays" then
		return Enum.RayCallbackResult.Fail -- Stop the raycast and act like we didn't hit anything.
	else
		return Enum.RayCallbackResult.Finished -- Raycast is finished, the results should be returned.
	end
end)

print("Raycast result:",hit,pos,norm,mat)
21 Likes

Also I’d like something that shows thickness of what was hit. It’d simplify things for me so much.

4 Likes

There’s so many edge cases with this stuff, its honestly bloat.

Would solve every single problem.

4 Likes

I don’t think this is the best way to implement this feature but we should definitely add something that helps with this use case.

This solution likely has the exact same performance as the current Lua based implementation that involes adding to the ignore list and raycasting again (but there might be some optimizations that I’m not thinking of that we could do).

I think a better solution would be to introduce the concept of raycast filters in Lua, this would be similar to the API that @Tomarty suggested in the thread that @sparker22 linked above.

Filters are used to filter out parts that might collide with the ray before the more expensive collision checks are done to see if the ray actually would have collided with that part. We could add a way to create a number of preset filters like PropertyFilter, WhitelistFilter, BlacklistFilter, CharacterFilter as well as a way to create a CustomFilter that uses a callback function. All these filters could be merged together with a MergedFilter so multiple filters could be applied.

With this system your code might look something like this:

local GeometryService = game:GetService("GeometryService")

local transparencyFilter = RaycastFilter.new(Enum.RaycastFilterType.PropertyFilter, "Transparency", Enum.RaycastFilterTest.LessThan, 0.95)

local canCollideFilter = RaycastFilter.new(Enum.RaycastFilterType.PropertyFilter, "CanCollide", Enum.RaycastFilterTest.Equality, true)

local blockRaysFilter = RaycastFilter.new(Enum.RaycastFilterType.PropertyFilter, "Name", Enum.RaycastFilterTest.Equality, "BlockRays") 

local mergedFilter = RaycastFilter.new(Enum.RaycastFilterType.MergedFilter, transparencyFilter, canCollideFilter, blockRaysFilter) 

local hit, pos, norm, mat = GeometryService:FindPartOnRayWithFilter(mergedFilter)

There are disadvantages to this system, mainly related to complexity and API design, but I think the performance gains would be worth it and it would be a lot nicer to use in a lot of common cases where repeated raycasts are currently used.

16 Likes

Consider that filters would have applications beyond raycasting (Region3, finding objects). A generic solution could be quite powerful.

-- Raycasting

local Transparent  = Filter.Property("Transparency", Enum.ComparisonOp.LessThan, 0.95)
local Uncollidable = Filter.Property("CanCollide", Enum.ComparisonOp.Equal, true)
local BlockRays    = Filter.Tag("BlockRays")
local AllowRays    = Filter.Tag("AllowRays")
local Model        = Filter.DescendantOf(Workspace.Model)

local propertyFilter = Filter.Group(Transparent, Enum.LogicalOp.And, Uncollidable)
local tagFilter      = Filter.Group(BlockRays, Enum.LogicalOp.And, Enum.LogicalOp.Not, AllowRays)
local raycastFilter  = Filter.Group(propertyFilter, Enum.LogicalOp.Or, tagFilter, Enum.LogicalOp.And, Enum.LogicalOp.Not, Model)
-- return (v.Transparency < 0.95 and v.CanCollide == true) or
-- (CollectionService:HasTag(v, "BlockRays") and not CollectionService:HasTag(v, "AllowRays") and
-- not v:IsDescendantOf(workspace.Model)

local hit, pos, norm, mat = Workspace:FindPartOnRayWithFilter(raycastFilter)
local invertedFilter = Filter.Group(Enum.LogicalOp.Not, raycastFilter) -- whitelist -> blacklist; powerful!
local parts = Workspace:FindPartsInRegion3WithFilter(invertedFilter)

-- Evaluate anything with filter
raycastFilter:Eval(workspace.Model.Part) --> false
invertedFilter:Eval(workspace.Model.Part) --> true


-- Selection

local humanoid = player.Character:FindFirstChildWithFilter(Filter.Class("Humanoid"))
-- Recursive; could also be named FindFirstDescendantWithFilter
local part = Workspace:FindFirstChildWithFilter(Filter.ClassOf("BasePart"), true)

local moduleFolder = game:GetService("ReplicatedStorage"):FindFirstChildWithFilter(Filter.Group(
	Filter.Class("Folder"),
	Enum.LogicalOp.And,
	Filter.Property("Name", Enum.ComparisonOp.Equal, "Modules")
))
local subFolders = moduleFolder:GetChildrenWithFilter(Filter.Class("Folder"))
local allModules = moduleFolder:GetDescendantsWithFilter(Filter.Class("ModuleScript"))

local screen = guiObject:FindFirstAncestorWithFilter(Filter.Class("ScreenGui"))
11 Likes

I can see this becoming incredibly useful in most things I do, I would adore this feature.

1 Like

In the way you’re demonstrating it, this might be bad behavior, since you’re creating those Filters on the fly.
The goal of this would be to have a complex configuration preset that Roblox can optimize on their end, that can then be reused multiple times. You might as well have actual Lua operations at that point.

EDIT: If it’s a singular use case in the execution, then I don’t think it’s terrible in principle, but in a condition where a filter is used multiple times, it would be in your best interest to have it pre-declared in order for the LuaBridge to work optimally.

I made a Filter module for funzies.

--[[

Filter.Property(name, op, value)
Filter.Tag(name)
Filter.DescendantOf(ancestor)
Filter.AncestorOf(descendant)
Filter.ChildOf(parent)
Filter.ParentOf(child)
Filter.Class(name)
Filter.ClassOf(name)
Filter.Callback(callback)
Filter.Group(...)

Filter.ComparisonOp.Equal
Filter.ComparisonOp.NotEqual
Filter.ComparisonOp.Less
Filter.ComparisonOp.LessOrEqual
Filter.ComparisonOp.Greater
Filter.ComparisonOp.GreaterOrEqual

Filter.LogicalOp.And
Filter.LogicalOp.Or
Filter.LogicalOp.Not

filter:Eval(instance)

]]

local ComparisonOps
local mtComparisonOps = {
	__tostring = function(self)
		return ComparisonOps[self]
	end,
}
local Equal          = setmetatable({}, mtComparisonOps)
local NotEqual       = setmetatable({}, mtComparisonOps)
local Less           = setmetatable({}, mtComparisonOps)
local LessOrEqual    = setmetatable({}, mtComparisonOps)
local Greater        = setmetatable({}, mtComparisonOps)
local GreaterOrEqual = setmetatable({}, mtComparisonOps)
ComparisonOps = {
	[Equal]          = "==",
	[NotEqual]       = "~=",
	[Less]           = "<",
	[LessOrEqual]    = "<=",
	[Greater]        = ">",
	[GreaterOrEqual] = ">=",
}
local compare = {
	[Equal]          = function(a, b) return a == b end,
	[NotEqual]       = function(a, b) return a ~= b end,
	[Less]           = function(a, b) return a < b  end,
	[LessOrEqual]    = function(a, b) return a <= b end,
	[Greater]        = function(a, b) return a > b  end,
	[GreaterOrEqual] = function(a, b) return a >= b end,
}

local LogicalOps
local mtLogicalOps = {
	__tostring = function(self)
		return LogicalOps[self]
	end,
}
local And = setmetatable({}, mtLogicalOps)
local Or  = setmetatable({}, mtLogicalOps)
local Not = setmetatable({}, mtLogicalOps)
LogicalOps = {
	[And] = "and",
	[Or]  = "or",
	[Not] = "not",
}

local Filter = {
	ComparisonOp = {
		Equal = Equal,
		NotEqual = NotEqual,
		Less = Less,
		LessOrEqual = LessOrEqual,
		Greater = Greater,
		GreaterOrEqual = GreaterOrEqual,
	},
	LogicalOp = {
		And = And,
		Or = Or,
		Not = Not,
	},
}

local filterTypes = {}

--------------------------------
--------------------------------

local function get(t, k)
	return t[k]
end
local mtFilterProperty = {
	__index = {
		Eval = function(self, instance)
			local ok, result = pcall(get, instance, self.name)
			if not ok then
				return false
			end
			return compare[self.op](result, self.value)
		end,
	},
	__tostring = function(self)
		return string.format("v.%s %s (%s)", self.name, tostring(self.op), tostring(self.value))
	end,
}
filterTypes[mtFilterProperty] = true
function Filter.Property(name, op, value)
	local filter = {
		name  = name,
		op    = op,
		value = value,
	}
	return setmetatable(filter, mtFilterProperty)
end

--------------------------------
--------------------------------

local CollectionService = game:GetService("CollectionService")
local mtFilterTag = {
	__index = {
		Eval = function(self, instance)
			return CollectionService:HasTag(instance, self.name)
		end,
	},
	__tostring = function(self)
		return string.format("CollectionService:HasTag(v, %q)", self.name)
	end,
}
filterTypes[mtFilterTag] = true
function Filter.Tag(name)
	local filter = {
		name  = name,
	}
	return setmetatable(filter, mtFilterTag)
end

--------------------------------
--------------------------------

local mtFilterDescendantOf = {
	__index = {
		Eval = function(self, instance)
			return instance:IsDescendantOf(self.instance)
		end,
	},
	__tostring = function(self)
		return string.format("v:IsDescendantOf(%s)", self.instance:GetFullName())
	end,
}
filterTypes[mtFilterDescendantOf] = true
function Filter.DescendantOf(instance)
	local filter = {
		instance = instance,
	}
	return setmetatable(filter, mtFilterDescendantOf)
end

--------------------------------
--------------------------------

local mtFilterAncestorOf = {
	__index = {
		Eval = function(self, instance)
			return instance:IsAncestorOf(self.instance)
		end,
	},
	__tostring = function(self)
		return string.format("v:IsAncestorOf(%s)", self.instance:GetFullName())
	end,
}
filterTypes[mtFilterAncestorOf] = true
function Filter.AncestorOf(instance)
	local filter = {
		instance = instance,
	}
	return setmetatable(filter, mtFilterAncestorOf)
end

--------------------------------
--------------------------------

local mtFilterChildOf = {
	__index = {
		Eval = function(self, instance)
			return instance.Parent == self.instance
		end,
	},
	__tostring = function(self)
		return string.format("v.Parent == %s", self.instance:GetFullName())
	end,
}
filterTypes[mtFilterChildOf] = true
function Filter.ChildOf(instance)
	local filter = {
		instance = instance,
	}
	return setmetatable(filter, mtFilterChildOf)
end

--------------------------------
--------------------------------

local mtFilterParentOf = {
	__index = {
		Eval = function(self, instance)
			return self.Parent == instance
		end,
	},
	__tostring = function(self)
		return string.format("%s.Parent == v", self.instance:GetFullName())
	end,
}
filterTypes[mtFilterParentOf] = true
function Filter.ParentOf(instance)
	local filter = {
		instance = instance,
	}
	return setmetatable(filter, mtFilterParentOf)
end

--------------------------------
--------------------------------

local mtFilterClass = {
	__index = {
		Eval = function(self, instance)
			return instance.ClassName == self.name
		end,
	},
	__tostring = function(self)
		return string.format("v.ClassName == %q", self.name)
	end,
}
filterTypes[mtFilterClass] = true
function Filter.Class(name)
	local filter = {
		name = name,
	}
	return setmetatable(filter, mtFilterClass)
end

--------------------------------
--------------------------------

local mtFilterClassOf = {
	__index = {
		Eval = function(self, instance)
			return instance:IsA(self.name)
		end,
	},
	__tostring = function(self)
		return string.format("v:IsA(%q)", self.name)
	end,
}
filterTypes[mtFilterClassOf] = true
function Filter.ClassOf(name)
	local filter = {
		name = name,
	}
	return setmetatable(filter, mtFilterClassOf)
end

--------------------------------
--------------------------------

local mtFilterCallback = {
	__index = {
		Eval = function(self, instance)
			return self.callback(instance)
		end,
	},
	__tostring = function(self)
		return "callback(v)"
	end,
}
filterTypes[mtFilterCallback] = true
function Filter.Callback(callback)
	local filter = {
		callback = callback,
	}
	return setmetatable(filter, mtFilterCallback)
end

--------------------------------
--------------------------------

local mtFilterGroup = {
	__index = {
		Eval = function(self, instance)
			local result = false
			local op = Or
			local items = self.items
			local i = 1
			while i <= #items do
				local c = true
				if items[i] == Not then
					i = i + 1
					if i > #items then
						return false
					end
					c = false
				end
				if not filterTypes[getmetatable(items[i])] then
					return false
				end
				if op == And then
					result = result and items[i]:Eval(instance) == c
				elseif op == Or then
					result = result or items[i]:Eval(instance) == c
				end
				i = i + 1
				if i < #items and not LogicalOps[items[i]] then
					return false
				end
				op = items[i]
				i = i + 1
			end
			if LogicalOps[items[#items]] then
				return false
			end
			return result
		end,
	},
	__tostring = function(self)
		local items = self.items
		if #items == 0 then
			return "(false)"
		end
		local result = "("
		local i = 1
		while i <= #items do
			if items[i] == Not then
				result = result .. tostring(items[i]) .. " "
				i = i + 1
				if i > #items then
					return false
				end
			end
			if not filterTypes[getmetatable(items[i])] then
				return "(false)"
			end
			result = result .. tostring(items[i])
			i = i + 1
			if i < #items and not LogicalOps[items[i]] then
				return "(false)"
			end
			if LogicalOps[items[i]] then
				result = result .. " " .. tostring(items[i]) .. " "
			end
			i = i + 1
		end
		if LogicalOps[items[#items]] then
			return "(false)"
		end
		return result .. ")"
	end,
}
filterTypes[mtFilterGroup] = true
function Filter.Group(...)
	local filter = {
		items = {},
	}

	local items = {...}
	if #items == 0 then
		error(string.format("group must contain at least one filter"))
	end
	local i = 1
	while i <= #items do
		if items[i] == Not then
			filter.items[i] = items[i]
			i = i + 1
			if i > #items then
				error(string.format("expected filter after argument #%d", i))
			end
		end
		if not filterTypes[getmetatable(items[i])] then
			error(string.format("expected filter at argument #%d", i))
		end
		filter.items[i] = items[i]
		i = i + 1
		if i < #items and not LogicalOps[items[i]] then
			error(string.format("expected LogicalOp at argument #%d", i))
		end
		filter.items[i] = items[i]
		i = i + 1
	end
	if LogicalOps[items[#items]] then
		error(string.format("expected filter after argument #%d", #items))
	end
	return setmetatable(filter, mtFilterGroup)
end

--------------------------------
--------------------------------

return Filter

When used with the previous example,

local Filter = require(game.ServerStorage.Filter)

local Transparent  = Filter.Property("Transparency", Filter.ComparisonOp.Less, 0.95)
local Uncollidable = Filter.Property("CanCollide", Filter.ComparisonOp.Equal, true)
local BlockRays    = Filter.Tag("BlockRays")
local AllowRays    = Filter.Tag("AllowRays")
local Model        = Filter.DescendantOf(workspace.Model)

local propertyFilter = Filter.Group(Transparent, Filter.LogicalOp.And, Uncollidable)
local tagFilter      = Filter.Group(BlockRays, Filter.LogicalOp.And, Filter.LogicalOp.Not, AllowRays)
local raycastFilter  = Filter.Group(propertyFilter, Filter.LogicalOp.Or, tagFilter, Filter.LogicalOp.And, Filter.LogicalOp.Not, Model)

print(Filter.Group(Filter.LogicalOp.Not, raycastFilter))
-- (not ((v.Transparency < (0.95) and v.CanCollide == (true)) or
-- (CollectionService:HasTag(v, "BlockRays") and 
-- not CollectionService:HasTag(v, "AllowRays")) and
-- not v:IsDescendantOf(Workspace.Model)))

10 Likes