Are rays supposed to have a fixed length when doing raycasts?

One of my friends told me that she couldn’t get raycasts to work when using Camera:ScreenPointToRay.
I looked into it, and found that it sends out a unit ray with a length of 1. The expected behavior is that this raycast can detect any part in front of it, regardless of distance. The actual behavior is that it only detects parts that are at most 1 stud in front of the ray.

I’ve known that rays behave this way for awhile, but I gotta ask: why? The API seems to imply that the directional vector is supposed to be a unit vector with no meaning to its length, and yet it acts like a line where the direction is the offset to from the origin, and the length is defined by the offset’s magnitude. This contradicts the definition of a ray.

To demonstrate the oddity of all this, I made a LocalScript that visualizes the result of ScreenPointToRay raycasts:

local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")

local debugPart = Instance.new("Part")
debugPart.Shape = "Ball"
debugPart.Material = "Neon"
debugPart.Anchored = true
debugPart.CanCollide = false
debugPart.Size = Vector3.new(.2,.2,.2)

local function step()
	local c = workspace.CurrentCamera
	if c then
		local mPos = UserInputService:GetMouseLocation()
		local ray = c:ScreenPointToRay(mPos.X,mPos.Y,0)
		local hit,pos = workspace:FindPartOnRay(ray,debugPart)
		debugPart.CFrame = CFrame.new(pos)
		debugPart.Color = Color3.new(hit and 0 or 1, hit and 1 or 0,0)
		debugPart.Parent = c
	end
end

RunService:BindToRenderStep("RayMouseTest",201,step)

The result is that the red sphere moves 1 stud away from the near clipping plane and then stops.

I can only make it work if the part I’m hitting is really close to the camera.

This issue can be “fixed” if you put this line under the local ray declaration:

ray = Ray.new(ray.Origin, ray.Direction * 5000)

I’m surprised no one has brought this issue up before. It’s weird that you have to reconstruct a ray in order to extend its “range”. Shoudn’t raycasts just assume that unit rays want the maximum length when performed?

This seems like a bug to me.

12 Likes

Huh, let me see if I can replicate the problem and I’ll get back to you. It might be a a bug.

This is the first thing I noticed when the API came out. I probably complained about it, somewhere.

2 Likes

It does the same thing for me as well.

1 Like

I just noticed that I typed workspace:FindPartOnRay(ray.Unit, debugPart) instead of workspace:FindPartOnRay(ray, debugPart). Its the same thing either way, I was just testing to see if it made a difference.

The max distance seems arbitrary. I think it’s OK as it is right now. Adding an extra parameter for the “length” of the ray would be cool, though.

I think there should be a constructor overload that uses a CFrame and a length that defaults to 5000.

4 Likes

I had a MASSIVE issue with this when trying to make a special camera system a while ago.

Sigh.

Here’s the post I made about it:

3 Likes

I’ve been thinking about this lately. This is a pretty important issue from an API usability standpoint, and there’s more than one way we could tackle it:

  • Make it easier to set the length of a ray
  • Make raycasts use an explicit distance param instead of the length of the ray

I’m leaning towards the latter.

Changing raycast behavior would probably mean adding a new API to avoid breaking existing games. This could be an interesting opportunity to move the raycasting API out of workspace and clean up some legacy stuff. We’ll see.

15 Likes

Came across this a couple weeks ago and it gave me such a headache. I made a debug part then noticed what was actually happening. Definitely a bug in my eyes.

I’d love to see an overhaul to the current raycasting system. Having 4 returned values in a tuple and several methods for handling blacklists/whitelists seems clumsy if you ask me.

6 Likes

It definitely is quite clumsy, but on the other hand the countless scripts breaking if this was changed seems like a lot of collateral damage to just accept for making something a bit easier to use.

The old system wouldn’t need to be changed, just deprecated.

1 Like

Seemed like a nuance of not replacing the existing thing but instead adding a new method is in order there though. A new method that does take these things into account I would definitely support of course.

What I’m thinking at the moment is a specific “Raycast” object.

It would have an integrated white/blacklist, a CFrame that could be changed, and then read-only result fields that are updated when a raycast call is made using the object. It could also have property filters, and perhaps it could also pick up Humanoids specifically.

The question comes down to how optimal a system like this would be.

4 Likes

Raycasts traditionally only need an origin and a direction. Can you elaborate on what you mean by a CFrame?

Well, it has more to do with the annoyances in the current Ray API.
A CFrame could define both the origin and direction (via lookVector). The object could also have a Vector3 Position/Direction property that reflects between the properties.

I had always assumed the reason for this was because of an optimization. I have no real evidence to back this up (since I’ve never seen the internal code), but it makes sense from an intuitive perspective at least if I were writing a ray class.

If the method for ray-casting was to find all intersections with geometry an obvious optimization would be to use a 3D tile like system and only pick the tiles that the ray passes through. That way you aren’t testing against geometry the ray has 0% chance of hitting. Since rays are world space it would make sense to define these tiles (or a reference to them) when constructing the ray as opposed to generating them every time you want to do the geometry intersection check. In other words the tiles never change, only the geometry inside them.

From there it would then make sense to return a unit ray as the default so that tons of tiles aren’t being calculated for no reason.

In an ideal world however I think the best solution is to just (as other people have suggested) add a new parameter.

Once again though I’ll stress that I have no idea if that whole tile concept is true so this is purely a guess…

3 Likes

One other thing I think a Raycast object could be beneficial for is garbage collection.

The current system of white/blacklists requires us to store the objects in an array. If we want to have a persistent array, then we have to manually remove the objects from the array in order to free them from memory, or just construct a temporary array each time we want to filter them. Either way, it’s a massive annoyance.

Even Roblox’s camera code has to deal with this in the PopperCam, because it has to ignore both the vehicle the player is in (if any), and the characters of every player in the game. It tries to optimize as best as it can, but it ultimately requires two arrays to be concatenated every time because regulating the contents of the array is already annoying as it is.

local ignoreList = {}
for _, character in pairs(PlayerCharacters) do
	ignoreList[#ignoreList + 1] = character
end
for i = 1, #VehicleParts do
	ignoreList[#ignoreList + 1] = VehicleParts[i]
end

A raycast object could open up the opportunity to improve this on the backend. The references to these objects could be stored in a lookup dictionary with weak_ptrs, thus removing the need for developers to manage the object references.

Any thoughts @Fractality_alt?

1 Like

All aboard the “FindPartOnRayWithIgnoreListAndSetDistance” hype train :slight_smile:

In all seriousness though, if you’re considering adding new raycasting API, consider exposing them in GeometryService, as workspace is getting crowded and the raycasting methods are defined in there anyway.